@ljwei-stak/dsh-model-route-bot 0.66.2 → 0.66.3
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/.dsh-plugin/client.js +9 -4
- package/README.i18n.yaml +2 -2
- package/README.md +6 -6
- package/README.zh.md +6 -6
- package/apps/integration-client/task-progress.mjs +11 -0
- package/apps/integration-client/tasks-panel.jsx +147 -147
- package/package.json +1 -1
- package/packages/model-router-plugin/README.md +3 -3
- package/packages/model-router-plugin/package.json +1 -1
package/.dsh-plugin/client.js
CHANGED
|
@@ -30933,7 +30933,7 @@ var xterm_default = `/**
|
|
|
30933
30933
|
`;
|
|
30934
30934
|
|
|
30935
30935
|
// .dsh-plugin/client/host-version.mjs
|
|
30936
|
-
var ROUTER_CLIENT_VERSION = "0.66.
|
|
30936
|
+
var ROUTER_CLIENT_VERSION = "0.66.3", STALE_HOST_MESSAGE = "插件后台版本较旧,请完全退出并重启 Harness(包括托盘图标)后再使用。";
|
|
30937
30937
|
function isMissingRemoteMethod(message2) {
|
|
30938
30938
|
let value = String(message2 ?? "");
|
|
30939
30939
|
return /transport failure for [^:]+: HTTP 404\b/.test(value) || /Remote method \S+ is no longer mounted/.test(value);
|
|
@@ -33373,7 +33373,12 @@ function botAttentionSummary(rows2 = []) {
|
|
|
33373
33373
|
}
|
|
33374
33374
|
|
|
33375
33375
|
// apps/integration-client/task-progress.mjs
|
|
33376
|
-
var taskIdOf = (row) => row.taskId ?? row.id, taskKey = (row) => JSON.stringify([row.botId ?? "", taskIdOf(row)]), taskTitle = (row) => row.title || row.input?.task || taskIdOf(row) || "未命名任务"
|
|
33376
|
+
var taskIdOf = (row) => row.taskId ?? row.id, taskKey = (row) => JSON.stringify([row.botId ?? "", taskIdOf(row)]), taskTitle = (row) => row.title || row.input?.task || taskIdOf(row) || "未命名任务";
|
|
33377
|
+
function progressNote(row, stateLabel = "") {
|
|
33378
|
+
let label = typeof row?.attribution?.label == "string" ? row.attribution.label.trim() : "";
|
|
33379
|
+
return label && label !== String(stateLabel ?? "").trim() ? label : "";
|
|
33380
|
+
}
|
|
33381
|
+
var TASK_FILTERS = Object.freeze([["all", "全部"], ["active", "进行中"], ["attention", "待处理"], ["finished", "已结束"]]), active = /* @__PURE__ */ new Set(["queued", "ready", "running", "admitting", "cancelling"]), ended = /* @__PURE__ */ new Set(["succeeded", "cancelled", "skipped"]);
|
|
33377
33382
|
function taskProgress(snapshot) {
|
|
33378
33383
|
let items = snapshot?.tasks?.status === "available" && Array.isArray(snapshot.tasks.items) ? snapshot.tasks.items : [], attention = deriveBotAttention({ ...snapshot, tasks: { items } }, { limit: 1 / 0 }), attentionKeys = new Set(attention.map((row) => taskKey(row))), category = (row) => attentionKeys.has(taskKey(row)) ? "attention" : active.has(row.state) ? "active" : ended.has(row.state) ? "finished" : "other", bots = new Map((snapshot?.bots ?? []).map((bot) => [bot.id, { botId: bot.id, name: bot.name, all: 0, active: 0, attention: 0, finished: 0, other: 0 }])), counts2 = { all: items.length, active: 0, attention: 0, finished: 0, other: 0 };
|
|
33379
33384
|
for (let row of items) {
|
|
@@ -33504,7 +33509,7 @@ function TaskDetail({ task, inspect, busy, actions, dispatch, onClose, bot, open
|
|
|
33504
33509
|
}), () => controller.abort());
|
|
33505
33510
|
}, [key, task.updatedAt, inspect, actions.includes("task.inspect"), reload]);
|
|
33506
33511
|
let data = history?.task ?? task;
|
|
33507
|
-
return /* @__PURE__ */ import_react63.default.createElement("article", { className: "bp-card bp-task-detail", "aria-label": "任务详情", ref, tabIndex: -1 }, /* @__PURE__ */ import_react63.default.createElement("div", { className: "bp-card-title" }, /* @__PURE__ */ import_react63.default.createElement("div", null, /* @__PURE__ */ import_react63.default.createElement("h2", null, "任务详情"), /* @__PURE__ */ import_react63.default.createElement("p", null, taskTitle(task))), /* @__PURE__ */ import_react63.default.createElement("button", { type: "button", onClick: onClose }, "关闭")), /* @__PURE__ */ import_react63.default.createElement("div", { className: "bp-actions" }, /* @__PURE__ */ import_react63.default.createElement(Tag2, null, TASK_STATES[data.state] ?? data.state), /* @__PURE__ */ import_react63.default.createElement("span", null, bot?.name ?? task.botId ?? "未取得归属"), bot?.sessionId && openSession && /* @__PURE__ */ import_react63.default.createElement("button", { type: "button", onClick: () => openSession(bot.sessionId) }, "打开 Bot 私聊"), /* @__PURE__ */ import_react63.default.createElement(TaskActions, { row: data, actions, dispatch, busy })), /* @__PURE__ */ import_react63.default.createElement(RemoteNotice, { row: data }), /* @__PURE__ */ import_react63.default.createElement("dl", { className: "bp-facts" }, /* @__PURE__ */ import_react63.default.createElement(Value, { label: "创建" }, date(data.createdAt)), /* @__PURE__ */ import_react63.default.createElement(Value, { label: "最近更新" }, date(data.updatedAt)), /* @__PURE__ */ import_react63.default.createElement(Value, { label: "期限" }, date(data.deadlineAt))), /* @__PURE__ */ import_react63.default.createElement("h3", null, "任务时间线"), loading && /* @__PURE__ */ import_react63.default.createElement("p", { role: "status", className: "bp-muted" }, "正在读取原任务日志…"), failure && /* @__PURE__ */ import_react63.default.createElement("p", { role: "alert", className: "bp-error" }, failure), inspect && actions.includes("task.inspect") && /* @__PURE__ */ import_react63.default.createElement("button", { type: "button", disabled: loading, onClick: () => setReload((value) => value + 1) }, "重新读取时间线"), !actions.includes("task.inspect") && /* @__PURE__ */ import_react63.default.createElement("p", { className: "bp-muted" }, "此宿主尚未提供任务时间线,可在下方查看现有收据依据。"), history && /* @__PURE__ */ import_react63.default.createElement(import_react63.default.Fragment, null, /* @__PURE__ */ import_react63.default.createElement("p", { className: "bp-muted" }, history.attempts.length, " 次执行尝试 · ", history.timeline.total, " 条日志", history.timeline.truncated ? `,显示最近 ${history.timeline.items.length} 条` : "", "。停止请求需等待原操作确认。"), /* @__PURE__ */ import_react63.default.createElement("ol", { className: "bp-task-timeline" }, history.timeline.items.map((event) => /* @__PURE__ */ import_react63.default.createElement("li", { key: event.seq }, /* @__PURE__ */ import_react63.default.createElement("span", { className: "bp-timeline-dot", "aria-hidden": "true" }), /* @__PURE__ */ import_react63.default.createElement("div", null, /* @__PURE__ */ import_react63.default.createElement("strong", null, TASK_EVENTS[event.kind] ?? "任务状态更新"), /* @__PURE__ */ import_react63.default.createElement("small", null, /* @__PURE__ */ import_react63.default.createElement("time", null, date(event.at)), event.attemptNumber != null ? ` · 第 ${event.attemptNumber} 次尝试` : "", event.outcome ? ` · ${TASK_STATES[event.outcome] ?? (event.outcome === "unknown" ? "结果未知" : event.outcome)}` : ""), event.code && /* @__PURE__ */ import_react63.default.createElement("small", { className: "bp-code" }, event.code))))), history.timeline.items.length === 0 && /* @__PURE__ */ import_react63.default.createElement("p", { className: "bp-muted" }, "原日志中暂无事件。"), /* @__PURE__ */ import_react63.default.createElement("details", { className: "bp-technical" }, /* @__PURE__ */ import_react63.default.createElement("summary", null, "每次尝试的身份与时间"), /* @__PURE__ */ import_react63.default.createElement("ul", { className: "bp-checklist" }, history.attempts.map((attempt) => /* @__PURE__ */ import_react63.default.createElement("li", { key: attempt.attemptId }, /* @__PURE__ */ import_react63.default.createElement("strong", null, "第 ", attempt.number, " 次 · ", TASK_STATES[attempt.state] ?? attempt.state), /* @__PURE__ */ import_react63.default.createElement("small", null, "开始 ", date(attempt.startedAt), " · 结束 ", date(attempt.finishedAt)), /* @__PURE__ */ import_react63.default.createElement("small", { className: "bp-code" }, attempt.attemptId, " · ", attempt.operationId)))))), /* @__PURE__ */ import_react63.default.createElement("details", { className: "bp-technical" }, /* @__PURE__ */ import_react63.default.createElement("summary", null, "原任务与操作编号"), /* @__PURE__ */ import_react63.default.createElement("dl", { className: "bp-facts" }, /* @__PURE__ */ import_react63.default.createElement(Value, { label: "Task" }, taskIdOf(data)), /* @__PURE__ */ import_react63.default.createElement(Value, { label: "Package" }, data.packageId), /* @__PURE__ */ import_react63.default.createElement(Value, { label: "Attempt" }, data.attemptId), /* @__PURE__ */ import_react63.default.createElement(Value, { label: "Operation" }, data.operationId), /* @__PURE__ */ import_react63.default.createElement(Value, { label: "MR 运行" }, data.legacyRunIds?.join("、"))
|
|
33512
|
+
return /* @__PURE__ */ import_react63.default.createElement("article", { className: "bp-card bp-task-detail", "aria-label": "任务详情", ref, tabIndex: -1 }, /* @__PURE__ */ import_react63.default.createElement("div", { className: "bp-card-title" }, /* @__PURE__ */ import_react63.default.createElement("div", null, /* @__PURE__ */ import_react63.default.createElement("h2", null, "任务详情"), /* @__PURE__ */ import_react63.default.createElement("p", null, taskTitle(task))), /* @__PURE__ */ import_react63.default.createElement("button", { type: "button", onClick: onClose }, "关闭")), /* @__PURE__ */ import_react63.default.createElement("div", { className: "bp-actions" }, /* @__PURE__ */ import_react63.default.createElement(Tag2, null, TASK_STATES[data.state] ?? data.state), /* @__PURE__ */ import_react63.default.createElement("span", null, bot?.name ?? task.botId ?? "未取得归属"), bot?.sessionId && openSession && /* @__PURE__ */ import_react63.default.createElement("button", { type: "button", onClick: () => openSession(bot.sessionId) }, "打开 Bot 私聊"), /* @__PURE__ */ import_react63.default.createElement(TaskActions, { row: data, actions, dispatch, busy })), /* @__PURE__ */ import_react63.default.createElement(RemoteNotice, { row: data }), /* @__PURE__ */ import_react63.default.createElement("dl", { className: "bp-facts" }, /* @__PURE__ */ import_react63.default.createElement(Value, { label: "创建" }, date(data.createdAt)), /* @__PURE__ */ import_react63.default.createElement(Value, { label: "最近更新" }, date(data.updatedAt)), /* @__PURE__ */ import_react63.default.createElement(Value, { label: "期限" }, date(data.deadlineAt)), data.attribution?.label && /* @__PURE__ */ import_react63.default.createElement(Value, { label: "执行进度" }, data.attribution.label)), /* @__PURE__ */ import_react63.default.createElement("h3", null, "任务时间线"), loading && /* @__PURE__ */ import_react63.default.createElement("p", { role: "status", className: "bp-muted" }, "正在读取原任务日志…"), failure && /* @__PURE__ */ import_react63.default.createElement("p", { role: "alert", className: "bp-error" }, failure), inspect && actions.includes("task.inspect") && /* @__PURE__ */ import_react63.default.createElement("button", { type: "button", disabled: loading, onClick: () => setReload((value) => value + 1) }, "重新读取时间线"), !actions.includes("task.inspect") && /* @__PURE__ */ import_react63.default.createElement("p", { className: "bp-muted" }, "此宿主尚未提供任务时间线,可在下方查看现有收据依据。"), history && /* @__PURE__ */ import_react63.default.createElement(import_react63.default.Fragment, null, /* @__PURE__ */ import_react63.default.createElement("p", { className: "bp-muted" }, history.attempts.length, " 次执行尝试 · ", history.timeline.total, " 条日志", history.timeline.truncated ? `,显示最近 ${history.timeline.items.length} 条` : "", "。停止请求需等待原操作确认。"), /* @__PURE__ */ import_react63.default.createElement("ol", { className: "bp-task-timeline" }, history.timeline.items.map((event) => /* @__PURE__ */ import_react63.default.createElement("li", { key: event.seq }, /* @__PURE__ */ import_react63.default.createElement("span", { className: "bp-timeline-dot", "aria-hidden": "true" }), /* @__PURE__ */ import_react63.default.createElement("div", null, /* @__PURE__ */ import_react63.default.createElement("strong", null, TASK_EVENTS[event.kind] ?? "任务状态更新"), /* @__PURE__ */ import_react63.default.createElement("small", null, /* @__PURE__ */ import_react63.default.createElement("time", null, date(event.at)), event.attemptNumber != null ? ` · 第 ${event.attemptNumber} 次尝试` : "", event.outcome ? ` · ${TASK_STATES[event.outcome] ?? (event.outcome === "unknown" ? "结果未知" : event.outcome)}` : ""), event.code && /* @__PURE__ */ import_react63.default.createElement("small", { className: "bp-code" }, event.code))))), history.timeline.items.length === 0 && /* @__PURE__ */ import_react63.default.createElement("p", { className: "bp-muted" }, "原日志中暂无事件。"), /* @__PURE__ */ import_react63.default.createElement("details", { className: "bp-technical" }, /* @__PURE__ */ import_react63.default.createElement("summary", null, "每次尝试的身份与时间"), /* @__PURE__ */ import_react63.default.createElement("ul", { className: "bp-checklist" }, history.attempts.map((attempt) => /* @__PURE__ */ import_react63.default.createElement("li", { key: attempt.attemptId }, /* @__PURE__ */ import_react63.default.createElement("strong", null, "第 ", attempt.number, " 次 · ", TASK_STATES[attempt.state] ?? attempt.state), /* @__PURE__ */ import_react63.default.createElement("small", null, "开始 ", date(attempt.startedAt), " · 结束 ", date(attempt.finishedAt)), /* @__PURE__ */ import_react63.default.createElement("small", { className: "bp-code" }, attempt.attemptId, " · ", attempt.operationId)))))), /* @__PURE__ */ import_react63.default.createElement("details", { className: "bp-technical" }, /* @__PURE__ */ import_react63.default.createElement("summary", null, "原任务与操作编号"), /* @__PURE__ */ import_react63.default.createElement("dl", { className: "bp-facts" }, /* @__PURE__ */ import_react63.default.createElement(Value, { label: "Task" }, taskIdOf(data)), /* @__PURE__ */ import_react63.default.createElement(Value, { label: "Package" }, data.packageId), /* @__PURE__ */ import_react63.default.createElement(Value, { label: "Attempt" }, data.attemptId), /* @__PURE__ */ import_react63.default.createElement(Value, { label: "Operation" }, data.operationId), /* @__PURE__ */ import_react63.default.createElement(Value, { label: "MR 运行" }, data.legacyRunIds?.join("、")))), /* @__PURE__ */ import_react63.default.createElement("details", { className: "bp-technical" }, /* @__PURE__ */ import_react63.default.createElement("summary", null, "执行与收据依据"), /* @__PURE__ */ import_react63.default.createElement("pre", null, JSON.stringify({ attempts: history?.attempts ?? task.attempts, executionRef: task.executionRef, remoteReceipt: data.result?.remote, reconciliation: data.reconciliation, packages: task.packages }, null, 2))));
|
|
33508
33513
|
}
|
|
33509
33514
|
function TaskWorkspace({ snapshot, dispatch, busy, scope, inspect, openSession, routingCard }) {
|
|
33510
33515
|
let progress = (0, import_react63.useMemo)(() => taskProgress(snapshot), [snapshot]), [botId, setBotId] = (0, import_react63.useState)(""), [status, setStatus] = (0, import_react63.useState)("all"), [query, setQuery] = (0, import_react63.useState)(""), [expanded, setExpanded] = (0, import_react63.useState)(!1), [selection, setSelection] = (0, import_react63.useState)(null);
|
|
@@ -33517,7 +33522,7 @@ function TaskWorkspace({ snapshot, dispatch, busy, scope, inspect, openSession,
|
|
|
33517
33522
|
return /* @__PURE__ */ import_react63.default.createElement("li", { key: taskKey(row), className: `bp-attention-row bp-attention-${row.stage}` }, /* @__PURE__ */ import_react63.default.createElement("div", null, /* @__PURE__ */ import_react63.default.createElement("strong", null, row.title), /* @__PURE__ */ import_react63.default.createElement("p", { className: "bp-muted" }, row.detail), row.stage === "approve" && /* @__PURE__ */ import_react63.default.createElement("small", null, "打开 Bot 私聊查看原审批请求。"), remoteUncertain(original) && /* @__PURE__ */ import_react63.default.createElement("small", null, "原操作仍待核对,请查看任务详情。")), /* @__PURE__ */ import_react63.default.createElement("div", { className: "bp-actions" }, /* @__PURE__ */ import_react63.default.createElement("button", { type: "button", onClick: () => select(original) }, "打开任务"), row.stage === "approve" && openSession && snapshot.bots?.find((bot) => bot.id === row.botId)?.sessionId && /* @__PURE__ */ import_react63.default.createElement("button", { type: "button", onClick: () => openSession(snapshot.bots.find((bot) => bot.id === row.botId).sessionId) }, "打开审批会话"), row.stage === "reconcile" && actions.includes("task.reconcile") && /* @__PURE__ */ import_react63.default.createElement(TaskActions, { row: original, actions: ["task.reconcile"], dispatch, busy }), row.stage === "failed" && actions.includes("task.retry") && /* @__PURE__ */ import_react63.default.createElement(TaskActions, { row: original, actions: ["task.retry"], dispatch, busy })));
|
|
33518
33523
|
})), progress.attention.length > 6 && /* @__PURE__ */ import_react63.default.createElement("button", { type: "button", "aria-expanded": expanded, onClick: () => setExpanded((value) => !value) }, expanded ? "收起待处理事项" : `查看全部 ${progress.attention.length} 项待处理`)), task && /* @__PURE__ */ import_react63.default.createElement(TaskDetail, { key: taskKey(task), task, inspect, busy, actions, dispatch, onClose: () => setSelection(null), bot: snapshot.bots?.find((bot) => bot.id === task.botId), openSession }), /* @__PURE__ */ import_react63.default.createElement("section", { className: "bp-card", "aria-label": "任务记录" }, /* @__PURE__ */ import_react63.default.createElement("div", { className: "bp-card-title" }, /* @__PURE__ */ import_react63.default.createElement("h2", null, "任务记录"), /* @__PURE__ */ import_react63.default.createElement("span", { className: "bp-muted", role: "status" }, "显示 ", rows2.length, " / ", progress.items.length, " 条")), /* @__PURE__ */ import_react63.default.createElement("div", { className: "bp-task-search" }, /* @__PURE__ */ import_react63.default.createElement("label", null, "搜索任务", /* @__PURE__ */ import_react63.default.createElement("input", { type: "search", value: query, placeholder: "任务名称、Bot 或任务 / 计划 / 操作编号", onChange: (event) => setQuery(event.target.value) })), (query || botId || status !== "all") && /* @__PURE__ */ import_react63.default.createElement("button", { type: "button", onClick: () => {
|
|
33519
33524
|
setQuery(""), setBotId(""), setStatus("all");
|
|
33520
|
-
} }, "清除筛选")), progress.items.length ? rows2.length === 0 ? /* @__PURE__ */ import_react63.default.createElement("p", { className: "bp-empty" }, "没有符合当前筛选的任务。可清除筛选查看其他记录。") : /* @__PURE__ */ import_react63.default.createElement("div", { className: "bp-table-wrap" }, /* @__PURE__ */ import_react63.default.createElement("table", null, /* @__PURE__ */ import_react63.default.createElement("thead", null, /* @__PURE__ */ import_react63.default.createElement("tr", null, /* @__PURE__ */ import_react63.default.createElement("th", null, "任务"), /* @__PURE__ */ import_react63.default.createElement("th", null, "Bot"), /* @__PURE__ */ import_react63.default.createElement("th", null, "状态"), /* @__PURE__ */ import_react63.default.createElement("th", null, "执行尝试"), /* @__PURE__ */ import_react63.default.createElement("th", null, "操作"))), /* @__PURE__ */ import_react63.default.createElement("tbody", null, rows2.map((row) => /* @__PURE__ */ import_react63.default.createElement("tr", { key: taskKey(row) }, /* @__PURE__ */ import_react63.default.createElement("td", null, /* @__PURE__ */ import_react63.default.createElement("button", { className: "bp-text-button", onClick: () => select(row) }, taskTitle(row)), /* @__PURE__ */ import_react63.default.createElement("small", null, "更新 ", date(row.updatedAt))), /* @__PURE__ */ import_react63.default.createElement("td", null, snapshot.bots?.find((bot) => bot.id === row.botId)?.name ?? row.botId ?? "未取得归属"), /* @__PURE__ */ import_react63.default.createElement("td", null, /* @__PURE__ */ import_react63.default.createElement(Tag2, null, TASK_STATES[row.state] ?? row.state), row.
|
|
33525
|
+
} }, "清除筛选")), progress.items.length ? rows2.length === 0 ? /* @__PURE__ */ import_react63.default.createElement("p", { className: "bp-empty" }, "没有符合当前筛选的任务。可清除筛选查看其他记录。") : /* @__PURE__ */ import_react63.default.createElement("div", { className: "bp-table-wrap" }, /* @__PURE__ */ import_react63.default.createElement("table", null, /* @__PURE__ */ import_react63.default.createElement("thead", null, /* @__PURE__ */ import_react63.default.createElement("tr", null, /* @__PURE__ */ import_react63.default.createElement("th", null, "任务"), /* @__PURE__ */ import_react63.default.createElement("th", null, "Bot"), /* @__PURE__ */ import_react63.default.createElement("th", null, "状态"), /* @__PURE__ */ import_react63.default.createElement("th", null, "执行尝试"), /* @__PURE__ */ import_react63.default.createElement("th", null, "操作"))), /* @__PURE__ */ import_react63.default.createElement("tbody", null, rows2.map((row) => /* @__PURE__ */ import_react63.default.createElement("tr", { key: taskKey(row) }, /* @__PURE__ */ import_react63.default.createElement("td", null, /* @__PURE__ */ import_react63.default.createElement("button", { className: "bp-text-button", onClick: () => select(row) }, taskTitle(row)), /* @__PURE__ */ import_react63.default.createElement("small", null, "更新 ", date(row.updatedAt))), /* @__PURE__ */ import_react63.default.createElement("td", null, snapshot.bots?.find((bot) => bot.id === row.botId)?.name ?? row.botId ?? "未取得归属"), /* @__PURE__ */ import_react63.default.createElement("td", null, /* @__PURE__ */ import_react63.default.createElement(Tag2, null, TASK_STATES[row.state] ?? row.state), progressNote(row, TASK_STATES[row.state] ?? row.state) && /* @__PURE__ */ import_react63.default.createElement("small", null, progressNote(row, TASK_STATES[row.state] ?? row.state)), row.blockedReason && /* @__PURE__ */ import_react63.default.createElement("small", null, row.blockedReason), /* @__PURE__ */ import_react63.default.createElement(RemoteNotice, { row })), /* @__PURE__ */ import_react63.default.createElement("td", null, Number.isSafeInteger(row.attempts) ? row.attempts : row.attempts?.length ?? row.attemptCount ?? "未知"), /* @__PURE__ */ import_react63.default.createElement("td", { className: "bp-actions" }, /* @__PURE__ */ import_react63.default.createElement(TaskActions, { row, actions, dispatch, busy }))))))) : /* @__PURE__ */ import_react63.default.createElement("p", { className: "bp-empty" }, "尚无持久任务。在 Main 私聊中提出目标后,可在这里查看工作包、执行尝试与收据。")), snapshot.tasks.plans?.length > 0 && /* @__PURE__ */ import_react63.default.createElement("section", { className: "bp-task-plans" }, /* @__PURE__ */ import_react63.default.createElement("button", { type: "button", "aria-expanded": showPlans, onClick: () => setShowPlans((value) => !value) }, showPlans ? "收起协作计划" : `查看 ${snapshot.tasks.plans.length} 个协作计划`), snapshot.tasks.plans.filter((plan) => showPlans || plan.needsReconciliation || !["succeeded", "cancelled", "failed"].includes(plan.state)).map((plan, index) => /* @__PURE__ */ import_react63.default.createElement("article", { className: "bp-card", key: plan.planId }, /* @__PURE__ */ import_react63.default.createElement("div", { className: "bp-card-title" }, /* @__PURE__ */ import_react63.default.createElement("div", null, /* @__PURE__ */ import_react63.default.createElement("h2", null, "协作计划 ", index + 1), /* @__PURE__ */ import_react63.default.createElement("p", { className: "bp-muted" }, plan.packages.length, " 个工作包 · ", plan.packages.filter((item) => item.state === "succeeded").length, " 个已完成")), /* @__PURE__ */ import_react63.default.createElement("div", { className: "bp-actions" }, /* @__PURE__ */ import_react63.default.createElement(Tag2, null, TASK_STATES[plan.state] ?? plan.state), actions.includes("task.plan.cancel") && !["succeeded", "cancelled"].includes(plan.state) && /* @__PURE__ */ import_react63.default.createElement("button", { disabled: busy, onClick: () => dispatch("task.plan.cancel", { botId: plan.ownerBotId, planId: plan.planId }) }, "停止整个计划"))), plan.needsReconciliation && /* @__PURE__ */ import_react63.default.createElement("p", { role: "status" }, "有工作包停止尚未确认,请核对原收据;计划尚未完成停止。"), /* @__PURE__ */ import_react63.default.createElement("ul", { className: "bp-plan-list" }, plan.packages.map((item, step) => {
|
|
33521
33526
|
let linked = progress.items.find((row) => taskIdOf(row) === item.taskId && row.botId === item.targetBotId);
|
|
33522
33527
|
return /* @__PURE__ */ import_react63.default.createElement("li", { key: item.id }, /* @__PURE__ */ import_react63.default.createElement("span", { className: "bp-plan-step" }, item.state === "succeeded" ? /* @__PURE__ */ import_react63.default.createElement(UiIcon, { name: "check", size: 13 }) : step + 1), /* @__PURE__ */ import_react63.default.createElement("div", { className: "bp-plan-copy" }, linked ? /* @__PURE__ */ import_react63.default.createElement("button", { className: "bp-text-button", onClick: () => select(linked) }, taskTitle(linked)) : /* @__PURE__ */ import_react63.default.createElement("strong", null, plan.plan?.packages?.find((pkg) => pkg.id === item.id)?.objective ?? `工作包 ${step + 1}`), /* @__PURE__ */ import_react63.default.createElement("small", null, snapshot.bots?.find((bot) => bot.id === item.targetBotId)?.name ?? item.targetBotId, item.dependsOn.length > 0 && ` · 前置:${item.dependsOn.join("、")}`, item.result?.code === "DEPENDENCY_FAILED" && " · 前置包失败,尚未执行")), /* @__PURE__ */ import_react63.default.createElement(Tag2, null, TASK_STATES[item.state] ?? item.state));
|
|
33523
33528
|
})), /* @__PURE__ */ import_react63.default.createElement("details", { className: "bp-technical bp-plan-id" }, /* @__PURE__ */ import_react63.default.createElement("summary", null, "计划编号"), /* @__PURE__ */ import_react63.default.createElement("p", { className: "bp-code" }, plan.planId))))), routingCard);
|
package/README.i18n.yaml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
# README translation pairing for this repository.
|
|
2
2
|
# Reviewed together; Git blob hashes identify the exact synchronized texts.
|
|
3
|
-
README.md:
|
|
4
|
-
README.zh.md:
|
|
3
|
+
README.md: 5e4513112263cb3fa934207db3574936eade9ec1
|
|
4
|
+
README.zh.md: f336244a5f9cd9b6166d455fd378984de72ea4a5
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Turn a task into a reviewable execution plan, select suitable configured models
|
|
|
14
14
|
|
|
15
15
|
This is a community plugin. It reuses Harness model configuration and Host services; each provider or CLI uses its own account and access rights.
|
|
16
16
|
|
|
17
|
-
**Current stable version: 0.66.
|
|
17
|
+
**Current stable version: 0.66.3 · Windows Remote Agent: 0.1.1**
|
|
18
18
|
|
|
19
19
|
[Choose an edition](#editions) · [Install](#install) · [First task](#quick-evaluation) · [Bot delegation](#bot-routing) · [Remote computers](#computers) · [Support](#troubleshooting)
|
|
20
20
|
|
|
@@ -52,8 +52,8 @@ Use **one edition per Harness profile**. Both editions share the routing configu
|
|
|
52
52
|
## Installation
|
|
53
53
|
|
|
54
54
|
1. Open **Plugins → Add plugin** in Harness Desktop and enter **one** package:
|
|
55
|
-
- Full workspace: `@ljwei-stak/dsh-model-route-bot@0.66.
|
|
56
|
-
- Routing only: `@ljwei-stak/dsh-model-router@0.66.
|
|
55
|
+
- Full workspace: `@ljwei-stak/dsh-model-route-bot@0.66.3`
|
|
56
|
+
- Routing only: `@ljwei-stak/dsh-model-router@0.66.3`
|
|
57
57
|
2. Enable it, fully quit Harness **including its tray process**, then restart so the Host loads the installed version.
|
|
58
58
|
3. Configure provider credentials and models in the official Harness **Models** settings. Open the Bot workspace for the full edition, or **Model Router** in the sidebar for the routing-only edition.
|
|
59
59
|
|
|
@@ -61,10 +61,10 @@ CLI installation is also supported. Choose the command for your edition:
|
|
|
61
61
|
|
|
62
62
|
```powershell
|
|
63
63
|
# Full Bot workspace
|
|
64
|
-
dsh plugin --profile desktop add @ljwei-stak/dsh-model-route-bot@0.66.
|
|
64
|
+
dsh plugin --profile desktop add @ljwei-stak/dsh-model-route-bot@0.66.3
|
|
65
65
|
|
|
66
66
|
# Routing-only workspace
|
|
67
|
-
dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.66.
|
|
67
|
+
dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.66.3
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
Alternatively, import the matching `.tgz` from [GitHub Releases](https://github.com/Alice-Marx/dsh-model-router/releases/latest). For an extracted local package, select the inner `package` directory containing `package.json` and `.dsh-plugin`. A global npm installation alone does not register a Harness plugin.
|
|
@@ -140,7 +140,7 @@ Integration mode reuses Harness Sessions. An optional independent runtime has it
|
|
|
140
140
|
|
|
141
141
|
## Multi-Bot task progress and pending items
|
|
142
142
|
|
|
143
|
-
**Available in the full Bot edition 0.66.
|
|
143
|
+
**Available in the full Bot edition 0.66.3.** On **Tasks**, inspect task counts per Bot, filter by **All / In progress / Needs attention / Ended**, or search task names and task, plan, or operation IDs. Counts and filters cover loaded records; funding admission is in progress, while budget waiting needs attention.
|
|
144
144
|
|
|
145
145
|
**Needs your attention** reports the full count and expands beyond six items. Each reminder opens the original task; approval reminders can open the owning Bot's conversation to show its original request. Open a task's **timeline** for queue, admission, execution, stop-request, and reconciliation events, with the original identities for each attempt. Reading history does not dispatch tasks or invoke models; stop requests still require confirmation. Older Hosts without history support retain existing receipt details.
|
|
146
146
|
|
package/README.zh.md
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
本项目是社区插件,复用 Harness 的模型配置和 Host 服务;各模型供应商与 CLI 使用各自的账户及访问权限。
|
|
16
16
|
|
|
17
|
-
**当前正式版:0.66.
|
|
17
|
+
**当前正式版:0.66.3 · Windows Remote Agent:0.1.1**
|
|
18
18
|
|
|
19
19
|
[选择版本](#editions) · [安装](#install) · [第一个任务](#quick-evaluation) · [Bot 分工](#bot-routing) · [远程电脑](#computers) · [问题反馈](#troubleshooting)
|
|
20
20
|
|
|
@@ -52,8 +52,8 @@
|
|
|
52
52
|
## 安装
|
|
53
53
|
|
|
54
54
|
1. 在 Harness Desktop 打开 **插件 → 添加插件**,输入以下包名之一:
|
|
55
|
-
- 完整工作区:`@ljwei-stak/dsh-model-route-bot@0.66.
|
|
56
|
-
- 仅模型路由:`@ljwei-stak/dsh-model-router@0.66.
|
|
55
|
+
- 完整工作区:`@ljwei-stak/dsh-model-route-bot@0.66.3`
|
|
56
|
+
- 仅模型路由:`@ljwei-stak/dsh-model-router@0.66.3`
|
|
57
57
|
2. 启用后,**连同托盘进程一起完全退出 Harness**,再重新启动,让 Host 加载已安装版本。
|
|
58
58
|
3. 在 Harness 官方 **模型** 设置中配置 Provider 凭据和模型。完整版本进入 Bot 工作区;纯路由版本从侧栏打开 **模型路由**。
|
|
59
59
|
|
|
@@ -61,10 +61,10 @@
|
|
|
61
61
|
|
|
62
62
|
```powershell
|
|
63
63
|
# 完整 Bot 工作区
|
|
64
|
-
dsh plugin --profile desktop add @ljwei-stak/dsh-model-route-bot@0.66.
|
|
64
|
+
dsh plugin --profile desktop add @ljwei-stak/dsh-model-route-bot@0.66.3
|
|
65
65
|
|
|
66
66
|
# 仅模型路由
|
|
67
|
-
dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.66.
|
|
67
|
+
dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.66.3
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
离线安装可从 [GitHub Releases](https://github.com/Alice-Marx/dsh-model-router/releases/latest) 导入对应 `.tgz`。本地解压安装时,选择同时包含 `package.json` 与 `.dsh-plugin` 的内层 `package` 目录。仅全局安装 npm 包不会将其注册为 Harness 插件。
|
|
@@ -140,7 +140,7 @@ dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.66.2
|
|
|
140
140
|
|
|
141
141
|
## 多 Bot 任务进度与待处理
|
|
142
142
|
|
|
143
|
-
**适用于完整 Bot 版本 0.66.
|
|
143
|
+
**适用于完整 Bot 版本 0.66.3。** 在 **任务** 页面按 Bot 查看任务数量,通过 **全部 / 进行中 / 待处理 / 已结束** 筛选,或搜索任务名称及任务、计划、操作编号。计数和筛选针对已加载记录;资金准入检查属于进行中,等待预算属于待处理。
|
|
144
144
|
|
|
145
145
|
**需要你处理** 显示完整数量,超过六项时可展开全部。每项都能打开原任务;等待审批时可进入对应 Bot 私聊查看原请求。打开任务后可查看 **任务时间线**,了解排队、资金准入、执行、停止请求及收据核对,并展开每次尝试的原编号。日志读取不会重新派发任务或调用模型;停止请求仍需等待确认。旧宿主未提供时间线时,页面保留现有收据详情。
|
|
146
146
|
|
|
@@ -3,6 +3,17 @@ import { deriveBotAttention } from './bot-attention.mjs'
|
|
|
3
3
|
export const taskIdOf = row => row.taskId ?? row.id
|
|
4
4
|
export const taskKey = row => JSON.stringify([row.botId ?? '', taskIdOf(row)])
|
|
5
5
|
export const taskTitle = row => row.title || row.input?.task || taskIdOf(row) || '未命名任务'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The one line the status cell adds under its tag. It says how far the work got,
|
|
9
|
+
* which the tag cannot: 已停止 does not tell you whether anything ran. When the
|
|
10
|
+
* attribution only repeats the tag it adds nothing, so it is not shown at all.
|
|
11
|
+
* The state label is passed in so this stays a pure function of the row.
|
|
12
|
+
*/
|
|
13
|
+
export function progressNote(row, stateLabel = '') {
|
|
14
|
+
const label = typeof row?.attribution?.label === 'string' ? row.attribution.label.trim() : ''
|
|
15
|
+
return label && label !== String(stateLabel ?? '').trim() ? label : ''
|
|
16
|
+
}
|
|
6
17
|
export const TASK_FILTERS = Object.freeze([['all', '全部'], ['active', '进行中'], ['attention', '待处理'], ['finished', '已结束']])
|
|
7
18
|
const active = new Set(['queued', 'ready', 'running', 'admitting', 'cancelling'])
|
|
8
19
|
const ended = new Set(['succeeded', 'cancelled', 'skipped'])
|
|
@@ -1,147 +1,147 @@
|
|
|
1
|
-
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
-
import { TASK_STATES } from './api.mjs'
|
|
3
|
-
import { botAttentionSummary, remoteUncertain } from './bot-attention.mjs'
|
|
4
|
-
import { filterTasks, taskProgress, taskIdOf, taskKey, taskTitle, TASK_FILTERS, TASK_EVENTS } from './task-progress.mjs'
|
|
5
|
-
import { UiIcon } from './ui-icons.jsx'
|
|
6
|
-
|
|
7
|
-
const Tag = ({ children }) => <span className="bp-tag">{children}</span>
|
|
8
|
-
const date = value => value === null || value === undefined || !Number.isFinite(new Date(value).getTime()) ? '未取得' : new Date(value).toLocaleString('zh-CN')
|
|
9
|
-
const Value = ({ label, children }) => <div className="bp-value"><dt>{label}</dt><dd>{children ?? '未取得'}</dd></div>
|
|
10
|
-
const stopKnown = row => !row.result?.remote || row.result.remote.stopConfirmed === true
|
|
11
|
-
|
|
12
|
-
function TaskActions({ row, actions, dispatch, busy }) {
|
|
13
|
-
const pending = useRef(false)
|
|
14
|
-
const run = async action => {
|
|
15
|
-
if (busy || pending.current || !dispatch || !actions.includes(action)) return
|
|
16
|
-
pending.current = true
|
|
17
|
-
try { await dispatch(action, { botId: row.botId, taskId: taskIdOf(row), ...(action === 'task.cancel' ? {} : { requestKey: crypto.randomUUID() }) }) }
|
|
18
|
-
finally { pending.current = false }
|
|
19
|
-
}
|
|
20
|
-
return <>
|
|
21
|
-
{actions.includes('task.cancel') && !['cancelled', 'succeeded', 'failed', 'skipped'].includes(row.state) && <button disabled={busy} onClick={() => { void run('task.cancel') }}>停止</button>}
|
|
22
|
-
{actions.includes('task.retry') && ['failed', 'cancelled', 'waiting-budget'].includes(row.state) && <button disabled={busy || !stopKnown(row)} onClick={() => { if (stopKnown(row)) void run('task.retry') }}>重试</button>}
|
|
23
|
-
{actions.includes('task.reconcile') && row.state === 'needs-reconciliation' && <button disabled={busy} onClick={() => { void run('task.reconcile') }}>核对原收据</button>}
|
|
24
|
-
</>
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function RemoteNotice({ row }) {
|
|
28
|
-
const remote = row.result?.remote
|
|
29
|
-
if (!remote) return null
|
|
30
|
-
if (!stopKnown(row)) return <small role="status">写入或停止尚未确认,请核对原操作;不会自动重发,也暂不能重试。</small>
|
|
31
|
-
if (remote.notStarted === true && remote.possiblyApplied !== true) return <small>原操作未启动。</small>
|
|
32
|
-
if (remote.status === 'succeeded') return <small>写入已确认。{row.state === 'cancelled' ? '取消未撤销文件变更。' : ''}</small>
|
|
33
|
-
return remote.possiblyApplied === true ? <small>操作已停止,文件可能已改变;重试不会撤销已有变更,请先核对产物。</small> : null
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function TaskDetail({ task, inspect, busy, actions, dispatch, onClose, bot, openSession }) {
|
|
37
|
-
const [history, setHistory] = useState(null), [failure, setFailure] = useState(''), [loading, setLoading] = useState(false)
|
|
38
|
-
const [reload, setReload] = useState(0)
|
|
39
|
-
const ref = useRef(null)
|
|
40
|
-
const key = taskKey(task)
|
|
41
|
-
useEffect(() => { ref.current?.focus({ preventScroll: true }); ref.current?.scrollIntoView?.({ block: 'nearest' }) }, [key])
|
|
42
|
-
useEffect(() => {
|
|
43
|
-
const controller = new AbortController()
|
|
44
|
-
setHistory(null); setFailure(''); setLoading(false)
|
|
45
|
-
if (!inspect || !actions.includes('task.inspect')) return () => controller.abort()
|
|
46
|
-
setLoading(true)
|
|
47
|
-
Promise.resolve().then(() => { if (!controller.signal.aborted) return inspect({ botId: task.botId, taskId: taskIdOf(task) }, controller.signal) }).then(value => {
|
|
48
|
-
if (controller.signal.aborted) return
|
|
49
|
-
if (!value?.task || taskKey(value.task) !== key || !Array.isArray(value.attempts) || !Array.isArray(value.timeline?.items)
|
|
50
|
-
|| (task.updatedAt != null && (!Number.isFinite(value.task.updatedAt) || value.task.updatedAt < task.updatedAt))) throw new Error('返回记录与当前任务不一致,请刷新后再次读取。')
|
|
51
|
-
setHistory(value)
|
|
52
|
-
}).catch(cause => { if (!controller.signal.aborted) setFailure(cause.message || '任务时间线读取失败。') })
|
|
53
|
-
.finally(() => { if (!controller.signal.aborted) setLoading(false) })
|
|
54
|
-
return () => controller.abort()
|
|
55
|
-
}, [key, task.updatedAt, inspect, actions.includes('task.inspect'), reload])
|
|
56
|
-
const data = history?.task ?? task
|
|
57
|
-
return <article className="bp-card bp-task-detail" aria-label="任务详情" ref={ref} tabIndex={-1}>
|
|
58
|
-
<div className="bp-card-title"><div><h2>任务详情</h2><p>{taskTitle(task)}</p></div><button type="button" onClick={onClose}>关闭</button></div>
|
|
59
|
-
<div className="bp-actions"><Tag>{TASK_STATES[data.state] ?? data.state}</Tag><span>{bot?.name ?? task.botId ?? '未取得归属'}</span>
|
|
60
|
-
{bot?.sessionId && openSession && <button type="button" onClick={() => openSession(bot.sessionId)}>打开 Bot 私聊</button>}
|
|
61
|
-
<TaskActions row={data} actions={actions} dispatch={dispatch} busy={busy} /></div>
|
|
62
|
-
<RemoteNotice row={data} />
|
|
63
|
-
<dl className="bp-facts"><Value label="创建">{date(data.createdAt)}</Value><Value label="最近更新">{date(data.updatedAt)}</Value><Value label="期限">{date(data.deadlineAt)}</Value
|
|
64
|
-
<h3>任务时间线</h3>
|
|
65
|
-
{loading && <p role="status" className="bp-muted">正在读取原任务日志…</p>}
|
|
66
|
-
{failure && <p role="alert" className="bp-error">{failure}</p>}
|
|
67
|
-
{inspect && actions.includes('task.inspect') && <button type="button" disabled={loading} onClick={() => setReload(value => value + 1)}>重新读取时间线</button>}
|
|
68
|
-
{!actions.includes('task.inspect') && <p className="bp-muted">此宿主尚未提供任务时间线,可在下方查看现有收据依据。</p>}
|
|
69
|
-
{history && <>
|
|
70
|
-
<p className="bp-muted">{history.attempts.length} 次执行尝试 · {history.timeline.total} 条日志{history.timeline.truncated ? `,显示最近 ${history.timeline.items.length} 条` : ''}。停止请求需等待原操作确认。</p>
|
|
71
|
-
<ol className="bp-task-timeline">{history.timeline.items.map(event => <li key={event.seq}>
|
|
72
|
-
<span className="bp-timeline-dot" aria-hidden="true" /><div><strong>{TASK_EVENTS[event.kind] ?? '任务状态更新'}</strong>
|
|
73
|
-
<small><time>{date(event.at)}</time>{event.attemptNumber != null ? ` · 第 ${event.attemptNumber} 次尝试` : ''}{event.outcome ? ` · ${TASK_STATES[event.outcome] ?? (event.outcome === 'unknown' ? '结果未知' : event.outcome)}` : ''}</small>
|
|
74
|
-
{event.code && <small className="bp-code">{event.code}</small>}</div>
|
|
75
|
-
</li>)}</ol>
|
|
76
|
-
{history.timeline.items.length === 0 && <p className="bp-muted">原日志中暂无事件。</p>}
|
|
77
|
-
<details className="bp-technical"><summary>每次尝试的身份与时间</summary><ul className="bp-checklist">{history.attempts.map(attempt => <li key={attempt.attemptId}>
|
|
78
|
-
<strong>第 {attempt.number} 次 · {TASK_STATES[attempt.state] ?? attempt.state}</strong><small>开始 {date(attempt.startedAt)} · 结束 {date(attempt.finishedAt)}</small><small className="bp-code">{attempt.attemptId} · {attempt.operationId}</small>
|
|
79
|
-
</li>)}</ul></details>
|
|
80
|
-
</>}
|
|
81
|
-
<details className="bp-technical"><summary>原任务与操作编号</summary><dl className="bp-facts"><Value label="Task">{taskIdOf(data)}</Value><Value label="Package">{data.packageId}</Value><Value label="Attempt">{data.attemptId}</Value><Value label="Operation">{data.operationId}</Value><Value label="MR 运行">{data.legacyRunIds?.join('、')}</Value
|
|
82
|
-
<details className="bp-technical"><summary>执行与收据依据</summary><pre>{JSON.stringify({ attempts: history?.attempts ?? task.attempts, executionRef: task.executionRef, remoteReceipt: data.result?.remote, reconciliation: data.reconciliation, packages: task.packages }, null, 2)}</pre></details>
|
|
83
|
-
</article>
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function TaskWorkspace({ snapshot, dispatch, busy, scope, inspect, openSession, routingCard }) {
|
|
87
|
-
const progress = useMemo(() => taskProgress(snapshot), [snapshot])
|
|
88
|
-
const [botId, setBotId] = useState(''), [status, setStatus] = useState('all'), [query, setQuery] = useState('')
|
|
89
|
-
const [expanded, setExpanded] = useState(false), [selection, setSelection] = useState(null)
|
|
90
|
-
useEffect(() => { setBotId(''); setStatus('all'); setQuery(''); setExpanded(false); setSelection(null) }, [scope])
|
|
91
|
-
const rows = filterTasks(progress, { botId, status, query })
|
|
92
|
-
const task = selection && selection.scope === scope ? progress.items.find(row => taskKey(row) === selection.key) : null
|
|
93
|
-
const actions = snapshot.availableActions ?? []
|
|
94
|
-
const select = row => setSelection({ scope, key: taskKey(row) })
|
|
95
|
-
const attention = expanded ? progress.attention : progress.attention.slice(0, 6)
|
|
96
|
-
const [showPlans, setShowPlans] = useState(false)
|
|
97
|
-
return <>
|
|
98
|
-
<section className="bp-card bp-task-overview" aria-label="多 Bot 任务总览">
|
|
99
|
-
<div className="bp-card-title"><div><h2>团队任务</h2><p className="bp-muted">按 Bot 查看进展,优先处理需要你介入的任务。</p></div><Tag>{progress.items.length} 条已加载</Tag></div>
|
|
100
|
-
{snapshot.tasks.truncated && <p role="status" className="bp-blocked">已加载 {progress.items.length} / {snapshot.tasks.total} 条任务,优先显示待处理与进行中的记录。下方数量与筛选仅针对已加载任务。</p>}
|
|
101
|
-
<div className="bp-task-counts" aria-label="按状态筛选任务">{TASK_FILTERS.map(([value, label]) => <button key={value} type="button" aria-pressed={status === value} onClick={() => setStatus(value)}><strong>{progress.counts[value]}</strong><span>{label}</span></button>)}</div>
|
|
102
|
-
<div className="bp-task-bots" aria-label="按 Bot 筛选任务"><button type="button" aria-pressed={!botId} onClick={() => setBotId('')}>全部 Bot</button>{progress.bots.map(bot => <button key={bot.botId ?? ''} type="button" aria-pressed={botId === bot.botId} onClick={() => setBotId(bot.botId ?? '')}>
|
|
103
|
-
<UiIcon name="bot" size={16} /><span>{bot.name}<small>{bot.active} 进行中 · {bot.attention} 待处理 · {bot.all} 条任务</small></span></button>)}</div>
|
|
104
|
-
</section>
|
|
105
|
-
{progress.attention.length > 0 && <section className="bp-card bp-attention" aria-label="需要你处理">
|
|
106
|
-
<div className="bp-card-title"><h2>需要你处理</h2><Tag>{botAttentionSummary(progress.attention)}</Tag></div>
|
|
107
|
-
<ul className="bp-attention-list">{attention.map(row => {
|
|
108
|
-
const original = progress.items.find(task => taskKey(task) === taskKey(row))
|
|
109
|
-
return <li key={taskKey(row)} className={`bp-attention-row bp-attention-${row.stage}`}><div><strong>{row.title}</strong><p className="bp-muted">{row.detail}</p>
|
|
110
|
-
{row.stage === 'approve' && <small>打开 Bot 私聊查看原审批请求。</small>}
|
|
111
|
-
{remoteUncertain(original) && <small>原操作仍待核对,请查看任务详情。</small>}</div><div className="bp-actions">
|
|
112
|
-
<button type="button" onClick={() => select(original)}>打开任务</button>
|
|
113
|
-
{row.stage === 'approve' && openSession && snapshot.bots?.find(bot => bot.id === row.botId)?.sessionId && <button type="button" onClick={() => openSession(snapshot.bots.find(bot => bot.id === row.botId).sessionId)}>打开审批会话</button>}
|
|
114
|
-
{row.stage === 'reconcile' && actions.includes('task.reconcile') && <TaskActions row={original} actions={['task.reconcile']} dispatch={dispatch} busy={busy} />}
|
|
115
|
-
{row.stage === 'failed' && actions.includes('task.retry') && <TaskActions row={original} actions={['task.retry']} dispatch={dispatch} busy={busy} />}
|
|
116
|
-
</div></li>
|
|
117
|
-
})}</ul>
|
|
118
|
-
{progress.attention.length > 6 && <button type="button" aria-expanded={expanded} onClick={() => setExpanded(value => !value)}>{expanded ? '收起待处理事项' : `查看全部 ${progress.attention.length} 项待处理`}</button>}
|
|
119
|
-
</section>}
|
|
120
|
-
{task && <TaskDetail key={taskKey(task)} task={task} inspect={inspect} busy={busy} actions={actions} dispatch={dispatch} onClose={() => setSelection(null)} bot={snapshot.bots?.find(bot => bot.id === task.botId)} openSession={openSession} />}
|
|
121
|
-
<section className="bp-card" aria-label="任务记录"><div className="bp-card-title"><h2>任务记录</h2><span className="bp-muted" role="status">显示 {rows.length} / {progress.items.length} 条</span></div>
|
|
122
|
-
<div className="bp-task-search"><label>搜索任务<input type="search" value={query} placeholder="任务名称、Bot 或任务 / 计划 / 操作编号" onChange={event => setQuery(event.target.value)} /></label>
|
|
123
|
-
{(query || botId || status !== 'all') && <button type="button" onClick={() => { setQuery(''); setBotId(''); setStatus('all') }}>清除筛选</button>}</div>
|
|
124
|
-
{!progress.items.length ? <p className="bp-empty">尚无持久任务。在 Main 私聊中提出目标后,可在这里查看工作包、执行尝试与收据。</p> : rows.length === 0 ? <p className="bp-empty">没有符合当前筛选的任务。可清除筛选查看其他记录。</p> : <div className="bp-table-wrap"><table>
|
|
125
|
-
<thead><tr><th>任务</th><th>Bot</th><th>状态</th><th>执行尝试</th><th>操作</th></tr></thead><tbody>{rows.map(row => <tr key={taskKey(row)}>
|
|
126
|
-
<td><button className="bp-text-button" onClick={() => select(row)}>{taskTitle(row)}</button><small>更新 {date(row.updatedAt)}</small></td>
|
|
127
|
-
<td>{snapshot.bots?.find(bot => bot.id === row.botId)?.name ?? row.botId ?? '未取得归属'}</td><td><Tag>{TASK_STATES[row.state] ?? row.state}</Tag>{row.
|
|
128
|
-
<td>{Number.isSafeInteger(row.attempts) ? row.attempts : row.attempts?.length ?? row.attemptCount ?? '未知'}</td><td className="bp-actions"><TaskActions row={row} actions={actions} dispatch={dispatch} busy={busy} /></td>
|
|
129
|
-
</tr>)}</tbody></table></div>}
|
|
130
|
-
</section>
|
|
131
|
-
{snapshot.tasks.plans?.length > 0 && <section className="bp-task-plans"><button type="button" aria-expanded={showPlans} onClick={() => setShowPlans(value => !value)}>{showPlans ? '收起协作计划' : `查看 ${snapshot.tasks.plans.length} 个协作计划`}</button>
|
|
132
|
-
{/* Dependency and cancellation evidence stays visible even when other plans are collapsed. */}
|
|
133
|
-
{snapshot.tasks.plans.filter(plan => showPlans || plan.needsReconciliation || !['succeeded', 'cancelled', 'failed'].includes(plan.state)).map((plan, index) => <article className="bp-card" key={plan.planId}>
|
|
134
|
-
<div className="bp-card-title"><div><h2>协作计划 {index + 1}</h2><p className="bp-muted">{plan.packages.length} 个工作包 · {plan.packages.filter(item => item.state === 'succeeded').length} 个已完成</p></div><div className="bp-actions"><Tag>{TASK_STATES[plan.state] ?? plan.state}</Tag>
|
|
135
|
-
{actions.includes('task.plan.cancel') && !['succeeded', 'cancelled'].includes(plan.state) && <button disabled={busy} onClick={() => dispatch('task.plan.cancel', { botId: plan.ownerBotId, planId: plan.planId })}>停止整个计划</button>}</div></div>
|
|
136
|
-
{plan.needsReconciliation && <p role="status">有工作包停止尚未确认,请核对原收据;计划尚未完成停止。</p>}
|
|
137
|
-
<ul className="bp-plan-list">{plan.packages.map((item, step) => {
|
|
138
|
-
const linked = progress.items.find(row => taskIdOf(row) === item.taskId && row.botId === item.targetBotId)
|
|
139
|
-
return <li key={item.id}><span className="bp-plan-step">{item.state === 'succeeded' ? <UiIcon name="check" size={13} /> : step + 1}</span><div className="bp-plan-copy">
|
|
140
|
-
{linked ? <button className="bp-text-button" onClick={() => select(linked)}>{taskTitle(linked)}</button> : <strong>{plan.plan?.packages?.find(pkg => pkg.id === item.id)?.objective ?? `工作包 ${step + 1}`}</strong>}
|
|
141
|
-
<small>{snapshot.bots?.find(bot => bot.id === item.targetBotId)?.name ?? item.targetBotId}{item.dependsOn.length > 0 && ` · 前置:${item.dependsOn.join('、')}`}{item.result?.code === 'DEPENDENCY_FAILED' && ' · 前置包失败,尚未执行'}</small></div><Tag>{TASK_STATES[item.state] ?? item.state}</Tag></li>
|
|
142
|
-
})}</ul><details className="bp-technical bp-plan-id"><summary>计划编号</summary><p className="bp-code">{plan.planId}</p></details>
|
|
143
|
-
</article>)}
|
|
144
|
-
</section>}
|
|
145
|
-
{routingCard}
|
|
146
|
-
</>
|
|
147
|
-
}
|
|
1
|
+
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
|
2
|
+
import { TASK_STATES } from './api.mjs'
|
|
3
|
+
import { botAttentionSummary, remoteUncertain } from './bot-attention.mjs'
|
|
4
|
+
import { filterTasks, progressNote, taskProgress, taskIdOf, taskKey, taskTitle, TASK_FILTERS, TASK_EVENTS } from './task-progress.mjs'
|
|
5
|
+
import { UiIcon } from './ui-icons.jsx'
|
|
6
|
+
|
|
7
|
+
const Tag = ({ children }) => <span className="bp-tag">{children}</span>
|
|
8
|
+
const date = value => value === null || value === undefined || !Number.isFinite(new Date(value).getTime()) ? '未取得' : new Date(value).toLocaleString('zh-CN')
|
|
9
|
+
const Value = ({ label, children }) => <div className="bp-value"><dt>{label}</dt><dd>{children ?? '未取得'}</dd></div>
|
|
10
|
+
const stopKnown = row => !row.result?.remote || row.result.remote.stopConfirmed === true
|
|
11
|
+
|
|
12
|
+
function TaskActions({ row, actions, dispatch, busy }) {
|
|
13
|
+
const pending = useRef(false)
|
|
14
|
+
const run = async action => {
|
|
15
|
+
if (busy || pending.current || !dispatch || !actions.includes(action)) return
|
|
16
|
+
pending.current = true
|
|
17
|
+
try { await dispatch(action, { botId: row.botId, taskId: taskIdOf(row), ...(action === 'task.cancel' ? {} : { requestKey: crypto.randomUUID() }) }) }
|
|
18
|
+
finally { pending.current = false }
|
|
19
|
+
}
|
|
20
|
+
return <>
|
|
21
|
+
{actions.includes('task.cancel') && !['cancelled', 'succeeded', 'failed', 'skipped'].includes(row.state) && <button disabled={busy} onClick={() => { void run('task.cancel') }}>停止</button>}
|
|
22
|
+
{actions.includes('task.retry') && ['failed', 'cancelled', 'waiting-budget'].includes(row.state) && <button disabled={busy || !stopKnown(row)} onClick={() => { if (stopKnown(row)) void run('task.retry') }}>重试</button>}
|
|
23
|
+
{actions.includes('task.reconcile') && row.state === 'needs-reconciliation' && <button disabled={busy} onClick={() => { void run('task.reconcile') }}>核对原收据</button>}
|
|
24
|
+
</>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function RemoteNotice({ row }) {
|
|
28
|
+
const remote = row.result?.remote
|
|
29
|
+
if (!remote) return null
|
|
30
|
+
if (!stopKnown(row)) return <small role="status">写入或停止尚未确认,请核对原操作;不会自动重发,也暂不能重试。</small>
|
|
31
|
+
if (remote.notStarted === true && remote.possiblyApplied !== true) return <small>原操作未启动。</small>
|
|
32
|
+
if (remote.status === 'succeeded') return <small>写入已确认。{row.state === 'cancelled' ? '取消未撤销文件变更。' : ''}</small>
|
|
33
|
+
return remote.possiblyApplied === true ? <small>操作已停止,文件可能已改变;重试不会撤销已有变更,请先核对产物。</small> : null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function TaskDetail({ task, inspect, busy, actions, dispatch, onClose, bot, openSession }) {
|
|
37
|
+
const [history, setHistory] = useState(null), [failure, setFailure] = useState(''), [loading, setLoading] = useState(false)
|
|
38
|
+
const [reload, setReload] = useState(0)
|
|
39
|
+
const ref = useRef(null)
|
|
40
|
+
const key = taskKey(task)
|
|
41
|
+
useEffect(() => { ref.current?.focus({ preventScroll: true }); ref.current?.scrollIntoView?.({ block: 'nearest' }) }, [key])
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const controller = new AbortController()
|
|
44
|
+
setHistory(null); setFailure(''); setLoading(false)
|
|
45
|
+
if (!inspect || !actions.includes('task.inspect')) return () => controller.abort()
|
|
46
|
+
setLoading(true)
|
|
47
|
+
Promise.resolve().then(() => { if (!controller.signal.aborted) return inspect({ botId: task.botId, taskId: taskIdOf(task) }, controller.signal) }).then(value => {
|
|
48
|
+
if (controller.signal.aborted) return
|
|
49
|
+
if (!value?.task || taskKey(value.task) !== key || !Array.isArray(value.attempts) || !Array.isArray(value.timeline?.items)
|
|
50
|
+
|| (task.updatedAt != null && (!Number.isFinite(value.task.updatedAt) || value.task.updatedAt < task.updatedAt))) throw new Error('返回记录与当前任务不一致,请刷新后再次读取。')
|
|
51
|
+
setHistory(value)
|
|
52
|
+
}).catch(cause => { if (!controller.signal.aborted) setFailure(cause.message || '任务时间线读取失败。') })
|
|
53
|
+
.finally(() => { if (!controller.signal.aborted) setLoading(false) })
|
|
54
|
+
return () => controller.abort()
|
|
55
|
+
}, [key, task.updatedAt, inspect, actions.includes('task.inspect'), reload])
|
|
56
|
+
const data = history?.task ?? task
|
|
57
|
+
return <article className="bp-card bp-task-detail" aria-label="任务详情" ref={ref} tabIndex={-1}>
|
|
58
|
+
<div className="bp-card-title"><div><h2>任务详情</h2><p>{taskTitle(task)}</p></div><button type="button" onClick={onClose}>关闭</button></div>
|
|
59
|
+
<div className="bp-actions"><Tag>{TASK_STATES[data.state] ?? data.state}</Tag><span>{bot?.name ?? task.botId ?? '未取得归属'}</span>
|
|
60
|
+
{bot?.sessionId && openSession && <button type="button" onClick={() => openSession(bot.sessionId)}>打开 Bot 私聊</button>}
|
|
61
|
+
<TaskActions row={data} actions={actions} dispatch={dispatch} busy={busy} /></div>
|
|
62
|
+
<RemoteNotice row={data} />
|
|
63
|
+
<dl className="bp-facts"><Value label="创建">{date(data.createdAt)}</Value><Value label="最近更新">{date(data.updatedAt)}</Value><Value label="期限">{date(data.deadlineAt)}</Value>{data.attribution?.label && <Value label="执行进度">{data.attribution.label}</Value>}</dl>
|
|
64
|
+
<h3>任务时间线</h3>
|
|
65
|
+
{loading && <p role="status" className="bp-muted">正在读取原任务日志…</p>}
|
|
66
|
+
{failure && <p role="alert" className="bp-error">{failure}</p>}
|
|
67
|
+
{inspect && actions.includes('task.inspect') && <button type="button" disabled={loading} onClick={() => setReload(value => value + 1)}>重新读取时间线</button>}
|
|
68
|
+
{!actions.includes('task.inspect') && <p className="bp-muted">此宿主尚未提供任务时间线,可在下方查看现有收据依据。</p>}
|
|
69
|
+
{history && <>
|
|
70
|
+
<p className="bp-muted">{history.attempts.length} 次执行尝试 · {history.timeline.total} 条日志{history.timeline.truncated ? `,显示最近 ${history.timeline.items.length} 条` : ''}。停止请求需等待原操作确认。</p>
|
|
71
|
+
<ol className="bp-task-timeline">{history.timeline.items.map(event => <li key={event.seq}>
|
|
72
|
+
<span className="bp-timeline-dot" aria-hidden="true" /><div><strong>{TASK_EVENTS[event.kind] ?? '任务状态更新'}</strong>
|
|
73
|
+
<small><time>{date(event.at)}</time>{event.attemptNumber != null ? ` · 第 ${event.attemptNumber} 次尝试` : ''}{event.outcome ? ` · ${TASK_STATES[event.outcome] ?? (event.outcome === 'unknown' ? '结果未知' : event.outcome)}` : ''}</small>
|
|
74
|
+
{event.code && <small className="bp-code">{event.code}</small>}</div>
|
|
75
|
+
</li>)}</ol>
|
|
76
|
+
{history.timeline.items.length === 0 && <p className="bp-muted">原日志中暂无事件。</p>}
|
|
77
|
+
<details className="bp-technical"><summary>每次尝试的身份与时间</summary><ul className="bp-checklist">{history.attempts.map(attempt => <li key={attempt.attemptId}>
|
|
78
|
+
<strong>第 {attempt.number} 次 · {TASK_STATES[attempt.state] ?? attempt.state}</strong><small>开始 {date(attempt.startedAt)} · 结束 {date(attempt.finishedAt)}</small><small className="bp-code">{attempt.attemptId} · {attempt.operationId}</small>
|
|
79
|
+
</li>)}</ul></details>
|
|
80
|
+
</>}
|
|
81
|
+
<details className="bp-technical"><summary>原任务与操作编号</summary><dl className="bp-facts"><Value label="Task">{taskIdOf(data)}</Value><Value label="Package">{data.packageId}</Value><Value label="Attempt">{data.attemptId}</Value><Value label="Operation">{data.operationId}</Value><Value label="MR 运行">{data.legacyRunIds?.join('、')}</Value></dl></details>
|
|
82
|
+
<details className="bp-technical"><summary>执行与收据依据</summary><pre>{JSON.stringify({ attempts: history?.attempts ?? task.attempts, executionRef: task.executionRef, remoteReceipt: data.result?.remote, reconciliation: data.reconciliation, packages: task.packages }, null, 2)}</pre></details>
|
|
83
|
+
</article>
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function TaskWorkspace({ snapshot, dispatch, busy, scope, inspect, openSession, routingCard }) {
|
|
87
|
+
const progress = useMemo(() => taskProgress(snapshot), [snapshot])
|
|
88
|
+
const [botId, setBotId] = useState(''), [status, setStatus] = useState('all'), [query, setQuery] = useState('')
|
|
89
|
+
const [expanded, setExpanded] = useState(false), [selection, setSelection] = useState(null)
|
|
90
|
+
useEffect(() => { setBotId(''); setStatus('all'); setQuery(''); setExpanded(false); setSelection(null) }, [scope])
|
|
91
|
+
const rows = filterTasks(progress, { botId, status, query })
|
|
92
|
+
const task = selection && selection.scope === scope ? progress.items.find(row => taskKey(row) === selection.key) : null
|
|
93
|
+
const actions = snapshot.availableActions ?? []
|
|
94
|
+
const select = row => setSelection({ scope, key: taskKey(row) })
|
|
95
|
+
const attention = expanded ? progress.attention : progress.attention.slice(0, 6)
|
|
96
|
+
const [showPlans, setShowPlans] = useState(false)
|
|
97
|
+
return <>
|
|
98
|
+
<section className="bp-card bp-task-overview" aria-label="多 Bot 任务总览">
|
|
99
|
+
<div className="bp-card-title"><div><h2>团队任务</h2><p className="bp-muted">按 Bot 查看进展,优先处理需要你介入的任务。</p></div><Tag>{progress.items.length} 条已加载</Tag></div>
|
|
100
|
+
{snapshot.tasks.truncated && <p role="status" className="bp-blocked">已加载 {progress.items.length} / {snapshot.tasks.total} 条任务,优先显示待处理与进行中的记录。下方数量与筛选仅针对已加载任务。</p>}
|
|
101
|
+
<div className="bp-task-counts" aria-label="按状态筛选任务">{TASK_FILTERS.map(([value, label]) => <button key={value} type="button" aria-pressed={status === value} onClick={() => setStatus(value)}><strong>{progress.counts[value]}</strong><span>{label}</span></button>)}</div>
|
|
102
|
+
<div className="bp-task-bots" aria-label="按 Bot 筛选任务"><button type="button" aria-pressed={!botId} onClick={() => setBotId('')}>全部 Bot</button>{progress.bots.map(bot => <button key={bot.botId ?? ''} type="button" aria-pressed={botId === bot.botId} onClick={() => setBotId(bot.botId ?? '')}>
|
|
103
|
+
<UiIcon name="bot" size={16} /><span>{bot.name}<small>{bot.active} 进行中 · {bot.attention} 待处理 · {bot.all} 条任务</small></span></button>)}</div>
|
|
104
|
+
</section>
|
|
105
|
+
{progress.attention.length > 0 && <section className="bp-card bp-attention" aria-label="需要你处理">
|
|
106
|
+
<div className="bp-card-title"><h2>需要你处理</h2><Tag>{botAttentionSummary(progress.attention)}</Tag></div>
|
|
107
|
+
<ul className="bp-attention-list">{attention.map(row => {
|
|
108
|
+
const original = progress.items.find(task => taskKey(task) === taskKey(row))
|
|
109
|
+
return <li key={taskKey(row)} className={`bp-attention-row bp-attention-${row.stage}`}><div><strong>{row.title}</strong><p className="bp-muted">{row.detail}</p>
|
|
110
|
+
{row.stage === 'approve' && <small>打开 Bot 私聊查看原审批请求。</small>}
|
|
111
|
+
{remoteUncertain(original) && <small>原操作仍待核对,请查看任务详情。</small>}</div><div className="bp-actions">
|
|
112
|
+
<button type="button" onClick={() => select(original)}>打开任务</button>
|
|
113
|
+
{row.stage === 'approve' && openSession && snapshot.bots?.find(bot => bot.id === row.botId)?.sessionId && <button type="button" onClick={() => openSession(snapshot.bots.find(bot => bot.id === row.botId).sessionId)}>打开审批会话</button>}
|
|
114
|
+
{row.stage === 'reconcile' && actions.includes('task.reconcile') && <TaskActions row={original} actions={['task.reconcile']} dispatch={dispatch} busy={busy} />}
|
|
115
|
+
{row.stage === 'failed' && actions.includes('task.retry') && <TaskActions row={original} actions={['task.retry']} dispatch={dispatch} busy={busy} />}
|
|
116
|
+
</div></li>
|
|
117
|
+
})}</ul>
|
|
118
|
+
{progress.attention.length > 6 && <button type="button" aria-expanded={expanded} onClick={() => setExpanded(value => !value)}>{expanded ? '收起待处理事项' : `查看全部 ${progress.attention.length} 项待处理`}</button>}
|
|
119
|
+
</section>}
|
|
120
|
+
{task && <TaskDetail key={taskKey(task)} task={task} inspect={inspect} busy={busy} actions={actions} dispatch={dispatch} onClose={() => setSelection(null)} bot={snapshot.bots?.find(bot => bot.id === task.botId)} openSession={openSession} />}
|
|
121
|
+
<section className="bp-card" aria-label="任务记录"><div className="bp-card-title"><h2>任务记录</h2><span className="bp-muted" role="status">显示 {rows.length} / {progress.items.length} 条</span></div>
|
|
122
|
+
<div className="bp-task-search"><label>搜索任务<input type="search" value={query} placeholder="任务名称、Bot 或任务 / 计划 / 操作编号" onChange={event => setQuery(event.target.value)} /></label>
|
|
123
|
+
{(query || botId || status !== 'all') && <button type="button" onClick={() => { setQuery(''); setBotId(''); setStatus('all') }}>清除筛选</button>}</div>
|
|
124
|
+
{!progress.items.length ? <p className="bp-empty">尚无持久任务。在 Main 私聊中提出目标后,可在这里查看工作包、执行尝试与收据。</p> : rows.length === 0 ? <p className="bp-empty">没有符合当前筛选的任务。可清除筛选查看其他记录。</p> : <div className="bp-table-wrap"><table>
|
|
125
|
+
<thead><tr><th>任务</th><th>Bot</th><th>状态</th><th>执行尝试</th><th>操作</th></tr></thead><tbody>{rows.map(row => <tr key={taskKey(row)}>
|
|
126
|
+
<td><button className="bp-text-button" onClick={() => select(row)}>{taskTitle(row)}</button><small>更新 {date(row.updatedAt)}</small></td>
|
|
127
|
+
<td>{snapshot.bots?.find(bot => bot.id === row.botId)?.name ?? row.botId ?? '未取得归属'}</td><td><Tag>{TASK_STATES[row.state] ?? row.state}</Tag>{progressNote(row, TASK_STATES[row.state] ?? row.state) && <small>{progressNote(row, TASK_STATES[row.state] ?? row.state)}</small>}{row.blockedReason && <small>{row.blockedReason}</small>}<RemoteNotice row={row} /></td>
|
|
128
|
+
<td>{Number.isSafeInteger(row.attempts) ? row.attempts : row.attempts?.length ?? row.attemptCount ?? '未知'}</td><td className="bp-actions"><TaskActions row={row} actions={actions} dispatch={dispatch} busy={busy} /></td>
|
|
129
|
+
</tr>)}</tbody></table></div>}
|
|
130
|
+
</section>
|
|
131
|
+
{snapshot.tasks.plans?.length > 0 && <section className="bp-task-plans"><button type="button" aria-expanded={showPlans} onClick={() => setShowPlans(value => !value)}>{showPlans ? '收起协作计划' : `查看 ${snapshot.tasks.plans.length} 个协作计划`}</button>
|
|
132
|
+
{/* Dependency and cancellation evidence stays visible even when other plans are collapsed. */}
|
|
133
|
+
{snapshot.tasks.plans.filter(plan => showPlans || plan.needsReconciliation || !['succeeded', 'cancelled', 'failed'].includes(plan.state)).map((plan, index) => <article className="bp-card" key={plan.planId}>
|
|
134
|
+
<div className="bp-card-title"><div><h2>协作计划 {index + 1}</h2><p className="bp-muted">{plan.packages.length} 个工作包 · {plan.packages.filter(item => item.state === 'succeeded').length} 个已完成</p></div><div className="bp-actions"><Tag>{TASK_STATES[plan.state] ?? plan.state}</Tag>
|
|
135
|
+
{actions.includes('task.plan.cancel') && !['succeeded', 'cancelled'].includes(plan.state) && <button disabled={busy} onClick={() => dispatch('task.plan.cancel', { botId: plan.ownerBotId, planId: plan.planId })}>停止整个计划</button>}</div></div>
|
|
136
|
+
{plan.needsReconciliation && <p role="status">有工作包停止尚未确认,请核对原收据;计划尚未完成停止。</p>}
|
|
137
|
+
<ul className="bp-plan-list">{plan.packages.map((item, step) => {
|
|
138
|
+
const linked = progress.items.find(row => taskIdOf(row) === item.taskId && row.botId === item.targetBotId)
|
|
139
|
+
return <li key={item.id}><span className="bp-plan-step">{item.state === 'succeeded' ? <UiIcon name="check" size={13} /> : step + 1}</span><div className="bp-plan-copy">
|
|
140
|
+
{linked ? <button className="bp-text-button" onClick={() => select(linked)}>{taskTitle(linked)}</button> : <strong>{plan.plan?.packages?.find(pkg => pkg.id === item.id)?.objective ?? `工作包 ${step + 1}`}</strong>}
|
|
141
|
+
<small>{snapshot.bots?.find(bot => bot.id === item.targetBotId)?.name ?? item.targetBotId}{item.dependsOn.length > 0 && ` · 前置:${item.dependsOn.join('、')}`}{item.result?.code === 'DEPENDENCY_FAILED' && ' · 前置包失败,尚未执行'}</small></div><Tag>{TASK_STATES[item.state] ?? item.state}</Tag></li>
|
|
142
|
+
})}</ul><details className="bp-technical bp-plan-id"><summary>计划编号</summary><p className="bp-code">{plan.planId}</p></details>
|
|
143
|
+
</article>)}
|
|
144
|
+
</section>}
|
|
145
|
+
{routingCard}
|
|
146
|
+
</>
|
|
147
|
+
}
|
package/package.json
CHANGED
|
@@ -8,16 +8,16 @@ English · [简体中文](README.zh.md)
|
|
|
8
8
|
|
|
9
9
|
Describe the result you want, choose suitable configured models or native CLI tools, review the plan, then follow execution and accounting from one workbench. This edition adds a **Model Router** sidebar entry and keeps the existing Harness workspace.
|
|
10
10
|
|
|
11
|
-
**Stable version: 0.66.
|
|
11
|
+
**Stable version: 0.66.3.** For Bot conversations, multi-Bot delegation, remote computers, private libraries, and routines, use the [full Bot edition](https://github.com/Alice-Marx/dsh-model-router/blob/main/README.md): **`@ljwei-stak/dsh-model-route-bot`**. Select one edition per profile.
|
|
12
12
|
|
|
13
13
|
## Install
|
|
14
14
|
|
|
15
|
-
1. In Harness **Plugins → Add plugin**, enter `@ljwei-stak/dsh-model-router@0.66.
|
|
15
|
+
1. In Harness **Plugins → Add plugin**, enter `@ljwei-stak/dsh-model-router@0.66.3`.
|
|
16
16
|
2. Enable it, quit Harness including its tray process, and restart.
|
|
17
17
|
3. Configure provider credentials/models in Harness **Models**, then open **Model Router** from the sidebar.
|
|
18
18
|
|
|
19
19
|
```powershell
|
|
20
|
-
dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.66.
|
|
20
|
+
dsh plugin --profile desktop add @ljwei-stak/dsh-model-router@0.66.3
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
## Run your first task
|