@ran-sh/dsh-crew 1.3.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/lib/client.js +502 -12
- package/package.json +1 -1
- package/src/client/history-panel.tsx +4 -4
- package/src/client/index.tsx +159 -7
- package/src/history/cleanup-plan.mjs +30 -5
- package/src/history/runner-detach.mjs +34 -0
- package/src/history/runtime.mjs +9 -2
- package/src/history/service.mjs +13 -2
- package/src/hub/index.mjs +55 -0
- package/src/install/install-legacy.mjs +8 -0
- package/src/install/install.mjs +13 -5
- package/src/jobs.mjs +24 -2
- package/src/model-routing.mjs +250 -147
- package/src/model-schedule.mjs +207 -0
- package/src/policy.mjs +69 -62
- package/src/runtime-identity.mjs +1 -1
- package/src/server.mjs +6 -1
- package/src/session-origins.mjs +60 -0
package/lib/client.js
CHANGED
|
@@ -570,8 +570,10 @@ const copy = {
|
|
|
570
570
|
operation: "操作",
|
|
571
571
|
archive: "归档(可恢复)",
|
|
572
572
|
delete: "删除(不可恢复)",
|
|
573
|
-
scope: "
|
|
574
|
-
all: "
|
|
573
|
+
scope: "范围",
|
|
574
|
+
all: "全部(含你自己的会话)",
|
|
575
|
+
crew: "仅 Crew 创建(推荐)",
|
|
576
|
+
worktree: "仅 Crew 工作树",
|
|
575
577
|
before: "指定时间之前",
|
|
576
578
|
time: "创建时间早于",
|
|
577
579
|
timeHint: "按本机时区选择,严格按创建时间筛选;包含新会话的工作区会保留。",
|
|
@@ -610,8 +612,10 @@ const copy = {
|
|
|
610
612
|
operation: "Action",
|
|
611
613
|
archive: "Archive (restorable)",
|
|
612
614
|
delete: "Delete (permanent)",
|
|
613
|
-
scope: "
|
|
614
|
-
all: "
|
|
615
|
+
scope: "Scope",
|
|
616
|
+
all: "Everything (includes your own sessions)",
|
|
617
|
+
crew: "Crew-created only (recommended)",
|
|
618
|
+
worktree: "Crew worktrees only",
|
|
615
619
|
before: "Before a date",
|
|
616
620
|
time: "Created before",
|
|
617
621
|
timeHint: "Local timezone; strict creation-time cutoff. Workspaces with newer sessions are kept.",
|
|
@@ -649,7 +653,7 @@ function HistoryPanel({ locale }) {
|
|
|
649
653
|
const t = copy[locale === "zh" ? "zh" : "en"];
|
|
650
654
|
const [open, setOpen] = (0, react.useState)(false);
|
|
651
655
|
const [operation, setOperation] = (0, react.useState)("archive");
|
|
652
|
-
const [scope, setScope] = (0, react.useState)("
|
|
656
|
+
const [scope, setScope] = (0, react.useState)("crew");
|
|
653
657
|
const [before, setBefore] = (0, react.useState)("");
|
|
654
658
|
const [preview, setPreview] = (0, react.useState)(null);
|
|
655
659
|
const [status, setStatus] = (0, react.useState)({ phase: "IDLE" });
|
|
@@ -793,13 +797,24 @@ function HistoryPanel({ locale }) {
|
|
|
793
797
|
setScope(e.target.value);
|
|
794
798
|
invalidate();
|
|
795
799
|
},
|
|
796
|
-
children: [
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
800
|
+
children: [
|
|
801
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
802
|
+
value: "crew",
|
|
803
|
+
children: t.crew
|
|
804
|
+
}),
|
|
805
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
806
|
+
value: "worktree",
|
|
807
|
+
children: t.worktree
|
|
808
|
+
}),
|
|
809
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
810
|
+
value: "all",
|
|
811
|
+
children: t.all
|
|
812
|
+
}),
|
|
813
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
814
|
+
value: "before",
|
|
815
|
+
children: t.before
|
|
816
|
+
})
|
|
817
|
+
]
|
|
803
818
|
})
|
|
804
819
|
] })]
|
|
805
820
|
}),
|
|
@@ -964,6 +979,148 @@ function HistoryPanel({ locale }) {
|
|
|
964
979
|
});
|
|
965
980
|
}
|
|
966
981
|
//#endregion
|
|
982
|
+
//#region src/model-schedule.mjs
|
|
983
|
+
/** Per-model restriction strength. Absent from the list means `off`. */
|
|
984
|
+
const MODEL_SCHEDULE_MODES = Object.freeze([
|
|
985
|
+
"off",
|
|
986
|
+
"warn",
|
|
987
|
+
"block"
|
|
988
|
+
]);
|
|
989
|
+
/** Peak means "expensive". Off-peak is every hour outside the windows. */
|
|
990
|
+
const DEFAULT_PEAK_WINDOWS = Object.freeze([Object.freeze({
|
|
991
|
+
start: "09:00",
|
|
992
|
+
end: "12:00"
|
|
993
|
+
}), Object.freeze({
|
|
994
|
+
start: "14:00",
|
|
995
|
+
end: "18:00"
|
|
996
|
+
})]);
|
|
997
|
+
const DEFAULT_PEAK_WEEKDAYS = Object.freeze([
|
|
998
|
+
1,
|
|
999
|
+
2,
|
|
1000
|
+
3,
|
|
1001
|
+
4,
|
|
1002
|
+
5
|
|
1003
|
+
]);
|
|
1004
|
+
const MAX_OFFSET_MINUTES = 840;
|
|
1005
|
+
/** "HH:MM" in 24-hour form to minutes past local midnight, or null. */
|
|
1006
|
+
function clockMinutes(value) {
|
|
1007
|
+
if (typeof value !== "string") return null;
|
|
1008
|
+
const match = /^(\d{1,2}):(\d{2})$/.exec(value.trim());
|
|
1009
|
+
if (!match) return null;
|
|
1010
|
+
const hours = Number(match[1]);
|
|
1011
|
+
const minutes = Number(match[2]);
|
|
1012
|
+
if (!Number.isInteger(hours) || !Number.isInteger(minutes)) return null;
|
|
1013
|
+
if (hours > 23 || minutes > 59) return null;
|
|
1014
|
+
return hours * 60 + minutes;
|
|
1015
|
+
}
|
|
1016
|
+
function offsetMinutes(value, fallback) {
|
|
1017
|
+
if (value === void 0 || value === null) return fallback;
|
|
1018
|
+
if (typeof value === "number" ? !Number.isInteger(value) : !/^-?\d+$/.test(String(value).trim())) return null;
|
|
1019
|
+
const parsed = typeof value === "number" ? value : Number.parseInt(String(value).trim(), 10);
|
|
1020
|
+
if (Math.abs(parsed) > MAX_OFFSET_MINUTES) return null;
|
|
1021
|
+
return parsed;
|
|
1022
|
+
}
|
|
1023
|
+
function weekdayList(value, fallback) {
|
|
1024
|
+
if (value === void 0 || value === null) return [...fallback];
|
|
1025
|
+
if (!Array.isArray(value)) return null;
|
|
1026
|
+
return [...new Set(value.filter((day) => Number.isInteger(day) && day >= 0 && day <= 6))].sort((a, b) => a - b);
|
|
1027
|
+
}
|
|
1028
|
+
function peakWindows(value, fallback) {
|
|
1029
|
+
if (value === void 0 || value === null) return fallback.map((window) => ({ ...window }));
|
|
1030
|
+
if (!Array.isArray(value)) return null;
|
|
1031
|
+
const windows = [];
|
|
1032
|
+
for (const entry of value) {
|
|
1033
|
+
const start = clockMinutes(entry?.start);
|
|
1034
|
+
const end = clockMinutes(entry?.end);
|
|
1035
|
+
if (start === null || end === null || start === end) continue;
|
|
1036
|
+
windows.push({
|
|
1037
|
+
start: entry.start.trim(),
|
|
1038
|
+
end: entry.end.trim()
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
return windows;
|
|
1042
|
+
}
|
|
1043
|
+
function modelModes(value) {
|
|
1044
|
+
if (!Array.isArray(value)) return [];
|
|
1045
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1046
|
+
const modes = [];
|
|
1047
|
+
for (const entry of value) {
|
|
1048
|
+
const provider = typeof entry?.provider === "string" ? entry.provider.trim() : "";
|
|
1049
|
+
const model = typeof entry?.model === "string" ? entry.model.trim() : "";
|
|
1050
|
+
const mode = typeof entry?.mode === "string" ? entry.mode.trim() : "";
|
|
1051
|
+
if (!provider || !model || !MODEL_SCHEDULE_MODES.includes(mode)) continue;
|
|
1052
|
+
if (mode === "off") continue;
|
|
1053
|
+
const key = `${provider}\0${model}`;
|
|
1054
|
+
if (seen.has(key)) continue;
|
|
1055
|
+
seen.add(key);
|
|
1056
|
+
modes.push({
|
|
1057
|
+
provider,
|
|
1058
|
+
model,
|
|
1059
|
+
mode
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
return modes;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* Coerce a stored or user-supplied schedule into the canonical shape.
|
|
1066
|
+
*
|
|
1067
|
+
* Lenient toward *missing* fields, strict toward *broken* ones. A missing field
|
|
1068
|
+
* gets its documented default; a field that is present but unusable (a string
|
|
1069
|
+
* where a list belongs, a non-integer offset) makes the whole schedule
|
|
1070
|
+
* non-restricting instead of substituting defaults. Substituting would let
|
|
1071
|
+
* accidental corruption switch on restrictions the operator never asked for.
|
|
1072
|
+
*/
|
|
1073
|
+
function normalizeModelSchedule(raw) {
|
|
1074
|
+
const source = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
|
|
1075
|
+
const models = modelModes(source.models);
|
|
1076
|
+
const offset = offsetMinutes(source.timezone_offset_minutes, 480);
|
|
1077
|
+
const weekdays = weekdayList(source.weekdays, DEFAULT_PEAK_WEEKDAYS);
|
|
1078
|
+
const windows = peakWindows(source.peak_windows, DEFAULT_PEAK_WINDOWS);
|
|
1079
|
+
if (offset === null || weekdays === null || windows === null) return {
|
|
1080
|
+
timezone_offset_minutes: offset === null ? 480 : offset,
|
|
1081
|
+
weekdays: [],
|
|
1082
|
+
peak_windows: windows === null ? [] : windows,
|
|
1083
|
+
models
|
|
1084
|
+
};
|
|
1085
|
+
return {
|
|
1086
|
+
timezone_offset_minutes: offset,
|
|
1087
|
+
weekdays,
|
|
1088
|
+
peak_windows: windows,
|
|
1089
|
+
models
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
/** Local wall clock for an instant under a fixed offset. */
|
|
1093
|
+
function localParts(at, offset) {
|
|
1094
|
+
const shifted = new Date(at.getTime() + offset * 6e4);
|
|
1095
|
+
return {
|
|
1096
|
+
weekday: shifted.getUTCDay(),
|
|
1097
|
+
minutes: shifted.getUTCHours() * 60 + shifted.getUTCMinutes()
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
function inWindow(minutes, start, end) {
|
|
1101
|
+
return start < end ? minutes >= start && minutes < end : minutes >= start || minutes < end;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Whether one instant falls in a peak window.
|
|
1105
|
+
*
|
|
1106
|
+
* A window crossing midnight also belongs to the weekday it starts on, so the
|
|
1107
|
+
* early-morning half is checked against the previous day.
|
|
1108
|
+
*/
|
|
1109
|
+
function isPeakAt(schedule, at = /* @__PURE__ */ new Date()) {
|
|
1110
|
+
const normalized = normalizeModelSchedule(schedule);
|
|
1111
|
+
if (normalized.weekdays.length === 0) return false;
|
|
1112
|
+
const { weekday, minutes } = localParts(at, normalized.timezone_offset_minutes);
|
|
1113
|
+
for (const window of normalized.peak_windows) {
|
|
1114
|
+
const start = clockMinutes(window.start);
|
|
1115
|
+
const end = clockMinutes(window.end);
|
|
1116
|
+
if (start === null || end === null) continue;
|
|
1117
|
+
if (!inWindow(minutes, start, end)) continue;
|
|
1118
|
+
const owner = start < end || minutes >= start ? weekday : (weekday + 6) % 7;
|
|
1119
|
+
if (normalized.weekdays.includes(owner)) return true;
|
|
1120
|
+
}
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
//#endregion
|
|
967
1124
|
//#region src/client/index.tsx
|
|
968
1125
|
const inject$1 = ["slots", "locale"];
|
|
969
1126
|
const API = "/_dsh/dsh-crew";
|
|
@@ -1020,6 +1177,7 @@ const COPY = {
|
|
|
1020
1177
|
flash: "Worker / Flash",
|
|
1021
1178
|
pro: "Reviewer / Pro",
|
|
1022
1179
|
dispatch: "模型优先级与派发",
|
|
1180
|
+
schedule: "波峰波谷调度",
|
|
1023
1181
|
adaptive: "自适应路由",
|
|
1024
1182
|
runtime: "运行 / 生效边界",
|
|
1025
1183
|
multimodal: "视觉与生图",
|
|
@@ -1291,6 +1449,22 @@ const COPY = {
|
|
|
1291
1449
|
adaptiveWindow: "健康窗口",
|
|
1292
1450
|
adaptiveSamples: "最少样本",
|
|
1293
1451
|
adaptiveBoundary: "下一个工作流生效",
|
|
1452
|
+
scheduleTitle: "波峰波谷调度",
|
|
1453
|
+
scheduleHint: "按本地时间限制指定模型:波峰时被“阻止”的模型会被跳过,由下一个候选接替任务;“提醒”的仍会使用并在选择轨迹中标记。未列出的模型不受限制。默认时段为 DeepSeek 公布的波峰时间(工作日 09:00-12:00、14:00-18:00)。",
|
|
1454
|
+
scheduleTimezone: "时区(相对 UTC)",
|
|
1455
|
+
scheduleWindows: "波峰时段",
|
|
1456
|
+
schedulePeak: "现在处于波峰",
|
|
1457
|
+
scheduleOffPeak: "现在处于波谷",
|
|
1458
|
+
scheduleAddWindow: "添加时段",
|
|
1459
|
+
scheduleStart: "开始",
|
|
1460
|
+
scheduleEnd: "结束",
|
|
1461
|
+
scheduleBlock: "阻止",
|
|
1462
|
+
scheduleWarn: "提醒",
|
|
1463
|
+
scheduleOff: "关闭",
|
|
1464
|
+
scheduleRestricted: "受限模型",
|
|
1465
|
+
scheduleNoRestricted: "还没有受限模型;未列出的模型不受时间限制。",
|
|
1466
|
+
scheduleWeekdays: "生效工作日",
|
|
1467
|
+
scheduleBoundary: "下次派发生效",
|
|
1294
1468
|
modelActivity: "模型活动概览",
|
|
1295
1469
|
modelActivityHint: "聚合本 Hub 内存中最近 500 个含真实调用证据的任务;每个模型按任务计数(不是推测底层轮次),最多显示 50 个模型。不保存提示词、结果或凭据。",
|
|
1296
1470
|
noModelActivity: "还没有真实模型活动。",
|
|
@@ -1364,6 +1538,7 @@ const COPY = {
|
|
|
1364
1538
|
flash: "Worker / Flash",
|
|
1365
1539
|
pro: "Reviewer / Pro",
|
|
1366
1540
|
dispatch: "Model priority & dispatch",
|
|
1541
|
+
schedule: "Peak / off-peak scheduling",
|
|
1367
1542
|
adaptive: "Adaptive routing",
|
|
1368
1543
|
runtime: "Runtime / activation boundaries",
|
|
1369
1544
|
multimodal: "Vision & image generation",
|
|
@@ -1635,6 +1810,22 @@ const COPY = {
|
|
|
1635
1810
|
adaptiveWindow: "Health window",
|
|
1636
1811
|
adaptiveSamples: "Minimum samples",
|
|
1637
1812
|
adaptiveBoundary: "Effective for the next workflow",
|
|
1813
|
+
scheduleTitle: "Peak / off-peak scheduling",
|
|
1814
|
+
scheduleHint: "Restricts chosen models by local wall clock. During peak, a \"block\" model is skipped and the next candidate serves the job; a \"warn\" model is still used and flagged in the selection trace. Models not listed are unrestricted. Defaults are DeepSeek's published peak hours (weekdays 09:00-12:00 and 14:00-18:00).",
|
|
1815
|
+
scheduleTimezone: "Timezone (relative to UTC)",
|
|
1816
|
+
scheduleWindows: "Peak windows",
|
|
1817
|
+
schedulePeak: "Peak hours now",
|
|
1818
|
+
scheduleOffPeak: "Off-peak now",
|
|
1819
|
+
scheduleAddWindow: "Add window",
|
|
1820
|
+
scheduleStart: "Start",
|
|
1821
|
+
scheduleEnd: "End",
|
|
1822
|
+
scheduleBlock: "Block",
|
|
1823
|
+
scheduleWarn: "Warn",
|
|
1824
|
+
scheduleOff: "Off",
|
|
1825
|
+
scheduleRestricted: "Restricted models",
|
|
1826
|
+
scheduleNoRestricted: "No restricted models yet; unlisted models have no time limit.",
|
|
1827
|
+
scheduleWeekdays: "Days the windows apply",
|
|
1828
|
+
scheduleBoundary: "Effective for the next dispatch",
|
|
1638
1829
|
modelActivity: "Model activity overview",
|
|
1639
1830
|
modelActivityHint: "Aggregates the latest 500 in-memory Hub jobs with real model-activity evidence; each model is counted once per qualifying job (not an inferred turn count). Shows at most 50 models; prompts, results, and credentials are never stored here.",
|
|
1640
1831
|
noModelActivity: "No real model activity yet.",
|
|
@@ -2964,6 +3155,74 @@ function WorkersPanel({ ctx }) {
|
|
|
2964
3155
|
ordering
|
|
2965
3156
|
} } });
|
|
2966
3157
|
};
|
|
3158
|
+
const schedule = normalizeModelSchedule(config?.model_schedule);
|
|
3159
|
+
const saveSchedule = (candidate) => {
|
|
3160
|
+
const next = normalizeModelSchedule(candidate);
|
|
3161
|
+
setConfig((current) => ({
|
|
3162
|
+
...current,
|
|
3163
|
+
model_schedule: next
|
|
3164
|
+
}));
|
|
3165
|
+
applyPatch({ model_schedule: next });
|
|
3166
|
+
};
|
|
3167
|
+
const schedulePeakNow = isPeakAt(schedule);
|
|
3168
|
+
const scheduleModeOf = (ref) => schedule.models.find((entry) => entry.provider === ref.provider && entry.model === ref.model)?.mode ?? "off";
|
|
3169
|
+
const setScheduleMode = (ref, mode) => {
|
|
3170
|
+
const others = schedule.models.filter((entry) => !(entry.provider === ref.provider && entry.model === ref.model));
|
|
3171
|
+
saveSchedule({
|
|
3172
|
+
...schedule,
|
|
3173
|
+
models: mode === "off" ? others : [...others, {
|
|
3174
|
+
...ref,
|
|
3175
|
+
mode
|
|
3176
|
+
}]
|
|
3177
|
+
});
|
|
3178
|
+
};
|
|
3179
|
+
const scheduleModelRefs = (() => {
|
|
3180
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3181
|
+
const add = (ref) => {
|
|
3182
|
+
if (ref?.provider && ref?.model) seen.set(`${ref.provider}\0${ref.model}`, {
|
|
3183
|
+
provider: ref.provider,
|
|
3184
|
+
model: ref.model
|
|
3185
|
+
});
|
|
3186
|
+
};
|
|
3187
|
+
for (const ref of config?.flash_model_priority ?? []) add(ref);
|
|
3188
|
+
for (const ref of config?.pro_model_priority ?? []) add(ref);
|
|
3189
|
+
for (const role of ["worker", "review"]) {
|
|
3190
|
+
const policy = config?.[role]?.model_policy;
|
|
3191
|
+
for (const ref of policy?.priority ?? []) add(ref);
|
|
3192
|
+
for (const ref of policy?.escalation_priority ?? []) add(ref);
|
|
3193
|
+
}
|
|
3194
|
+
add(modelCatalog?.harness_default);
|
|
3195
|
+
for (const provider of modelCatalog?.providers ?? []) for (const model of provider?.models ?? []) add({
|
|
3196
|
+
provider: provider.id,
|
|
3197
|
+
model: model?.id
|
|
3198
|
+
});
|
|
3199
|
+
for (const entry of schedule.models) add(entry);
|
|
3200
|
+
return [...seen.values()];
|
|
3201
|
+
})();
|
|
3202
|
+
const scheduleWeekdayNames = locale === "zh" ? [
|
|
3203
|
+
"日",
|
|
3204
|
+
"一",
|
|
3205
|
+
"二",
|
|
3206
|
+
"三",
|
|
3207
|
+
"四",
|
|
3208
|
+
"五",
|
|
3209
|
+
"六"
|
|
3210
|
+
] : [
|
|
3211
|
+
"Sun",
|
|
3212
|
+
"Mon",
|
|
3213
|
+
"Tue",
|
|
3214
|
+
"Wed",
|
|
3215
|
+
"Thu",
|
|
3216
|
+
"Fri",
|
|
3217
|
+
"Sat"
|
|
3218
|
+
];
|
|
3219
|
+
const toggleScheduleWeekday = (day) => {
|
|
3220
|
+
const days = schedule.weekdays.includes(day) ? schedule.weekdays.filter((value) => value !== day) : [...schedule.weekdays, day].sort((a, b) => a - b);
|
|
3221
|
+
saveSchedule({
|
|
3222
|
+
...schedule,
|
|
3223
|
+
weekdays: days
|
|
3224
|
+
});
|
|
3225
|
+
};
|
|
2967
3226
|
const modelActivity = aggregateModelInvocations(jobs);
|
|
2968
3227
|
const modelState = modelCallabilityState(readinessSnapshot?.model_callability, runtimeInfo);
|
|
2969
3228
|
const currentSurfaceResponsibilities = surfaceResponsibilities(surface);
|
|
@@ -4078,6 +4337,237 @@ function WorkersPanel({ ctx }) {
|
|
|
4078
4337
|
})
|
|
4079
4338
|
] }))
|
|
4080
4339
|
}),
|
|
4340
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleSection, {
|
|
4341
|
+
sectionId: "schedule",
|
|
4342
|
+
title: copy.sectionNames.schedule,
|
|
4343
|
+
summary: sectionSummary(schedulePeakNow ? copy.schedulePeak : copy.scheduleOffPeak, `UTC${schedule.timezone_offset_minutes >= 0 ? "+" : "-"}${Math.abs(schedule.timezone_offset_minutes) / 60}`, config?.model_schedule === void 0 ? copy.scheduleOff : `${schedule.models.length} · ${copy.scheduleRestricted}`),
|
|
4344
|
+
expanded: !!expandedSections.schedule,
|
|
4345
|
+
onToggle: () => toggleSection("schedule"),
|
|
4346
|
+
children: block({
|
|
4347
|
+
t: copy.scheduleTitle,
|
|
4348
|
+
d: copy.scheduleHint
|
|
4349
|
+
}, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
4350
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
4351
|
+
style: S.field,
|
|
4352
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4353
|
+
style: S.fieldLabel,
|
|
4354
|
+
children: copy.scheduleTimezone
|
|
4355
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
|
|
4356
|
+
value: String(schedule.timezone_offset_minutes),
|
|
4357
|
+
onChange: (v) => saveSchedule({
|
|
4358
|
+
...schedule,
|
|
4359
|
+
timezone_offset_minutes: Number(v)
|
|
4360
|
+
}),
|
|
4361
|
+
options: (() => {
|
|
4362
|
+
const offsets = /* @__PURE__ */ new Set();
|
|
4363
|
+
for (let h = -14; h <= 14; h++) offsets.add(h * 60);
|
|
4364
|
+
for (const minutes of [
|
|
4365
|
+
330,
|
|
4366
|
+
345,
|
|
4367
|
+
570,
|
|
4368
|
+
-210,
|
|
4369
|
+
-570
|
|
4370
|
+
]) offsets.add(minutes);
|
|
4371
|
+
offsets.add(schedule.timezone_offset_minutes);
|
|
4372
|
+
return [...offsets].sort((a, b) => a - b).map((minutes) => ({
|
|
4373
|
+
value: String(minutes),
|
|
4374
|
+
label: `UTC${minutes >= 0 ? "+" : "-"}${Math.floor(Math.abs(minutes) / 60)}` + (Math.abs(minutes) % 60 ? `:${String(Math.abs(minutes) % 60).padStart(2, "0")}` : "")
|
|
4375
|
+
}));
|
|
4376
|
+
})()
|
|
4377
|
+
})]
|
|
4378
|
+
}),
|
|
4379
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4380
|
+
style: { gridColumn: "1 / -1" },
|
|
4381
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4382
|
+
style: {
|
|
4383
|
+
...S.fieldLabel,
|
|
4384
|
+
display: "block",
|
|
4385
|
+
marginBottom: 5
|
|
4386
|
+
},
|
|
4387
|
+
children: copy.scheduleWeekdays
|
|
4388
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4389
|
+
style: {
|
|
4390
|
+
display: "flex",
|
|
4391
|
+
gap: 5,
|
|
4392
|
+
flexWrap: "wrap"
|
|
4393
|
+
},
|
|
4394
|
+
children: scheduleWeekdayNames.map((name, day) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4395
|
+
type: "button",
|
|
4396
|
+
style: {
|
|
4397
|
+
...S.btn,
|
|
4398
|
+
padding: "3px 9px",
|
|
4399
|
+
opacity: schedule.weekdays.includes(day) ? 1 : .45
|
|
4400
|
+
},
|
|
4401
|
+
onClick: () => toggleScheduleWeekday(day),
|
|
4402
|
+
children: name
|
|
4403
|
+
}, day))
|
|
4404
|
+
})]
|
|
4405
|
+
}),
|
|
4406
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4407
|
+
style: { gridColumn: "1 / -1" },
|
|
4408
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4409
|
+
style: {
|
|
4410
|
+
...S.fieldLabel,
|
|
4411
|
+
display: "block",
|
|
4412
|
+
marginBottom: 5
|
|
4413
|
+
},
|
|
4414
|
+
children: copy.scheduleWindows
|
|
4415
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4416
|
+
style: {
|
|
4417
|
+
display: "flex",
|
|
4418
|
+
flexDirection: "column",
|
|
4419
|
+
gap: 6
|
|
4420
|
+
},
|
|
4421
|
+
children: [schedule.peak_windows.map((window, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4422
|
+
style: {
|
|
4423
|
+
display: "flex",
|
|
4424
|
+
gap: 6,
|
|
4425
|
+
alignItems: "center"
|
|
4426
|
+
},
|
|
4427
|
+
children: [
|
|
4428
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4429
|
+
type: "time",
|
|
4430
|
+
value: window.start,
|
|
4431
|
+
style: S.input,
|
|
4432
|
+
onChange: (event) => {
|
|
4433
|
+
const next = schedule.peak_windows.map((row, i) => i === index ? {
|
|
4434
|
+
...row,
|
|
4435
|
+
start: event.target.value
|
|
4436
|
+
} : row);
|
|
4437
|
+
saveSchedule({
|
|
4438
|
+
...schedule,
|
|
4439
|
+
peak_windows: next
|
|
4440
|
+
});
|
|
4441
|
+
}
|
|
4442
|
+
}),
|
|
4443
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4444
|
+
style: { opacity: .6 },
|
|
4445
|
+
children: "–"
|
|
4446
|
+
}),
|
|
4447
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
4448
|
+
type: "time",
|
|
4449
|
+
value: window.end,
|
|
4450
|
+
style: S.input,
|
|
4451
|
+
onChange: (event) => {
|
|
4452
|
+
const next = schedule.peak_windows.map((row, i) => i === index ? {
|
|
4453
|
+
...row,
|
|
4454
|
+
end: event.target.value
|
|
4455
|
+
} : row);
|
|
4456
|
+
saveSchedule({
|
|
4457
|
+
...schedule,
|
|
4458
|
+
peak_windows: next
|
|
4459
|
+
});
|
|
4460
|
+
}
|
|
4461
|
+
}),
|
|
4462
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4463
|
+
type: "button",
|
|
4464
|
+
style: {
|
|
4465
|
+
...S.btn,
|
|
4466
|
+
padding: "2px 8px"
|
|
4467
|
+
},
|
|
4468
|
+
onClick: () => saveSchedule({
|
|
4469
|
+
...schedule,
|
|
4470
|
+
peak_windows: schedule.peak_windows.filter((_, i) => i !== index)
|
|
4471
|
+
}),
|
|
4472
|
+
children: "×"
|
|
4473
|
+
})
|
|
4474
|
+
]
|
|
4475
|
+
}, `${window.start}-${window.end}-${index}`)), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
4476
|
+
type: "button",
|
|
4477
|
+
style: {
|
|
4478
|
+
...S.btn,
|
|
4479
|
+
alignSelf: "flex-start"
|
|
4480
|
+
},
|
|
4481
|
+
onClick: () => saveSchedule({
|
|
4482
|
+
...schedule,
|
|
4483
|
+
peak_windows: [...schedule.peak_windows, {
|
|
4484
|
+
start: "09:00",
|
|
4485
|
+
end: "12:00"
|
|
4486
|
+
}]
|
|
4487
|
+
}),
|
|
4488
|
+
children: copy.scheduleAddWindow
|
|
4489
|
+
})]
|
|
4490
|
+
})]
|
|
4491
|
+
}),
|
|
4492
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4493
|
+
style: { gridColumn: "1 / -1" },
|
|
4494
|
+
children: [
|
|
4495
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4496
|
+
style: {
|
|
4497
|
+
...S.fieldLabel,
|
|
4498
|
+
display: "block",
|
|
4499
|
+
marginBottom: 5
|
|
4500
|
+
},
|
|
4501
|
+
children: copy.scheduleRestricted
|
|
4502
|
+
}),
|
|
4503
|
+
scheduleModelRefs.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4504
|
+
style: {
|
|
4505
|
+
fontSize: 11.5,
|
|
4506
|
+
opacity: .6
|
|
4507
|
+
},
|
|
4508
|
+
children: copy.scheduleNoRestricted
|
|
4509
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4510
|
+
style: {
|
|
4511
|
+
display: "flex",
|
|
4512
|
+
flexDirection: "column",
|
|
4513
|
+
gap: 5
|
|
4514
|
+
},
|
|
4515
|
+
children: scheduleModelRefs.map((ref) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4516
|
+
style: {
|
|
4517
|
+
display: "flex",
|
|
4518
|
+
gap: 8,
|
|
4519
|
+
alignItems: "center",
|
|
4520
|
+
padding: "5px 8px",
|
|
4521
|
+
border: "1px solid rgba(128,128,128,0.2)",
|
|
4522
|
+
borderRadius: 6
|
|
4523
|
+
},
|
|
4524
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4525
|
+
style: {
|
|
4526
|
+
flex: 1,
|
|
4527
|
+
minWidth: 0,
|
|
4528
|
+
fontSize: 11,
|
|
4529
|
+
fontFamily: "monospace",
|
|
4530
|
+
overflow: "hidden",
|
|
4531
|
+
textOverflow: "ellipsis"
|
|
4532
|
+
},
|
|
4533
|
+
children: [
|
|
4534
|
+
ref.provider,
|
|
4535
|
+
" / ",
|
|
4536
|
+
ref.model
|
|
4537
|
+
]
|
|
4538
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomSelect, {
|
|
4539
|
+
value: scheduleModeOf(ref),
|
|
4540
|
+
onChange: (v) => setScheduleMode(ref, v),
|
|
4541
|
+
options: [
|
|
4542
|
+
{
|
|
4543
|
+
value: "off",
|
|
4544
|
+
label: copy.scheduleOff
|
|
4545
|
+
},
|
|
4546
|
+
{
|
|
4547
|
+
value: "warn",
|
|
4548
|
+
label: copy.scheduleWarn
|
|
4549
|
+
},
|
|
4550
|
+
{
|
|
4551
|
+
value: "block",
|
|
4552
|
+
label: copy.scheduleBlock
|
|
4553
|
+
}
|
|
4554
|
+
]
|
|
4555
|
+
})]
|
|
4556
|
+
}, `${ref.provider}\0${ref.model}`))
|
|
4557
|
+
}),
|
|
4558
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
4559
|
+
style: {
|
|
4560
|
+
display: "block",
|
|
4561
|
+
fontSize: 11,
|
|
4562
|
+
opacity: .55,
|
|
4563
|
+
marginTop: 5
|
|
4564
|
+
},
|
|
4565
|
+
children: copy.scheduleBoundary
|
|
4566
|
+
})
|
|
4567
|
+
]
|
|
4568
|
+
})
|
|
4569
|
+
] }))
|
|
4570
|
+
}),
|
|
4081
4571
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CollapsibleSection, {
|
|
4082
4572
|
sectionId: "runtime",
|
|
4083
4573
|
title: copy.sectionNames.runtime,
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ const pending = (phase?: string) => !!phase && !['IDLE', 'DONE', 'FAILED', 'ROLL
|
|
|
5
5
|
const copy = {
|
|
6
6
|
zh: {
|
|
7
7
|
title: '工作区与会话清理', intro: '仅影响 3210 的工作区记录与会话日志;不删除项目文件、附件或 3080 数据。',
|
|
8
|
-
operation: '操作', archive: '归档(可恢复)', delete: '删除(不可恢复)', scope: '
|
|
8
|
+
operation: '操作', archive: '归档(可恢复)', delete: '删除(不可恢复)', scope: '范围', all: '全部(含你自己的会话)', crew: '仅 Crew 创建(推荐)', worktree: '仅 Crew 工作树', before: '指定时间之前',
|
|
9
9
|
time: '创建时间早于', timeHint: '按本机时区选择,严格按创建时间筛选;包含新会话的工作区会保留。',
|
|
10
10
|
preview: '预览清理范围', confirm: '确认执行', acknowledgement: '删除确认:请输入 DELETE', idle: '会短暂停止并重启 3210;请先结束或关闭所有正在使用的 3210 会话。',
|
|
11
11
|
consent: '我已确认范围,并同意短暂重启 3210', restore: '恢复', archives: '已归档批次', empty: '暂无可恢复的归档',
|
|
@@ -17,7 +17,7 @@ const copy = {
|
|
|
17
17
|
},
|
|
18
18
|
en: {
|
|
19
19
|
title: 'Workspace & session cleanup', intro: 'Only 3210 workspace records and session logs. Project files, attachments and 3080 data are untouched.',
|
|
20
|
-
operation: 'Action', archive: 'Archive (restorable)', delete: 'Delete (permanent)', scope: '
|
|
20
|
+
operation: 'Action', archive: 'Archive (restorable)', delete: 'Delete (permanent)', scope: 'Scope', all: 'Everything (includes your own sessions)', crew: 'Crew-created only (recommended)', worktree: 'Crew worktrees only', before: 'Before a date',
|
|
21
21
|
time: 'Created before', timeHint: 'Local timezone; strict creation-time cutoff. Workspaces with newer sessions are kept.',
|
|
22
22
|
preview: 'Preview cleanup', confirm: 'Confirm operation', acknowledgement: 'Type DELETE to confirm deletion', idle: '3210 will briefly stop and restart. End or close all active 3210 conversations first.',
|
|
23
23
|
consent: 'I reviewed the scope and agree to restart 3210', restore: 'Restore', archives: 'Archived batches', empty: 'No restorable archives',
|
|
@@ -33,7 +33,7 @@ export function HistoryPanel({ locale }: { locale: string }) {
|
|
|
33
33
|
const t = copy[locale === 'zh' ? 'zh' : 'en'];
|
|
34
34
|
const [open, setOpen] = useState(false);
|
|
35
35
|
const [operation, setOperation] = useState('archive');
|
|
36
|
-
const [scope, setScope] = useState('
|
|
36
|
+
const [scope, setScope] = useState('crew');
|
|
37
37
|
const [before, setBefore] = useState('');
|
|
38
38
|
const [preview, setPreview] = useState<any>(null);
|
|
39
39
|
const [status, setStatus] = useState<any>({ phase: 'IDLE' });
|
|
@@ -81,7 +81,7 @@ export function HistoryPanel({ locale }: { locale: string }) {
|
|
|
81
81
|
<p style={{ margin: 0, opacity: .75 }}>{t.intro}</p>
|
|
82
82
|
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
|
83
83
|
<label>{t.operation} <select style={style} value={operation} disabled={locked} onChange={e => { setOperation(e.target.value); invalidate(); }}><option value="archive">{t.archive}</option><option value="delete">{t.delete}</option></select></label>
|
|
84
|
-
<label>{t.scope} <select style={style} value={scope} disabled={locked} onChange={e => { setScope(e.target.value); invalidate(); }}><option value="all">{t.all}</option><option value="before">{t.before}</option></select></label>
|
|
84
|
+
<label>{t.scope} <select style={style} value={scope} disabled={locked} onChange={e => { setScope(e.target.value); invalidate(); }}><option value="crew">{t.crew}</option><option value="worktree">{t.worktree}</option><option value="all">{t.all}</option><option value="before">{t.before}</option></select></label>
|
|
85
85
|
</div>
|
|
86
86
|
{scope === 'before' && <label>{t.time} <input type="datetime-local" style={style} value={before} disabled={locked} onChange={e => { setBefore(e.target.value); invalidate(); }} /></label>}
|
|
87
87
|
<div style={{ fontSize: 11.5, opacity: .7 }}>{t.timeHint}</div>
|