dsh-plugin-t-expert 0.3.24 → 0.3.26

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.
Files changed (56) hide show
  1. package/README.md +9 -7
  2. package/data/.t-team-snapshot.json +10 -0
  3. package/data/experts/engineering/engineering-security-engineer.md +303 -0
  4. package/data/experts/engineering/engineering-threat-detection-engineer.md +553 -0
  5. package/data/experts/finance/finance-financial-forecaster.md +213 -0
  6. package/data/experts/hr/hr-recruiter.md +232 -0
  7. package/data/experts/marketing/marketing-search-growth-orchestrator.md +372 -0
  8. package/data/experts/specialized/specialized-pricing-optimizer.md +254 -0
  9. package/data/experts/support/support-recruitment-specialist.md +508 -0
  10. package/data/source.json +2 -2
  11. package/data/t-team.config.json +826 -1126
  12. package/data/teams.json +486 -672
  13. package/data/teams.resolved.json +743 -1109
  14. package/data/zh/COVERAGE.json +4 -4
  15. package/data/zh/company/chief-of-staff.md +278 -0
  16. package/data/zh/descriptions.json +21 -14
  17. package/data/zh/divisions.json +3 -3
  18. package/data/zh/engineering/engineering-security-engineer.md +303 -0
  19. package/data/zh/engineering/engineering-threat-detection-engineer.md +553 -0
  20. package/data/zh/finance/finance-financial-forecaster.md +213 -0
  21. package/data/zh/hr/hr-recruiter.md +232 -0
  22. package/data/zh/manual.json +4 -4
  23. package/data/zh/marketing/marketing-aeo-foundations.md +489 -219
  24. package/data/zh/marketing/marketing-ai-citation-strategist.md +692 -140
  25. package/data/zh/marketing/marketing-bilibili-strategist.md +194 -0
  26. package/data/zh/marketing/marketing-search-growth-orchestrator.md +372 -0
  27. package/data/zh/marketing/marketing-seo-specialist.md +427 -245
  28. package/data/zh/names.json +21 -14
  29. package/data/zh/specialized/recruitment-specialist.md +490 -31
  30. package/data/zh/specialized/specialized-french-consulting-market.md +173 -28
  31. package/data/zh/specialized/specialized-korean-business-navigator.md +198 -29
  32. package/data/zh/specialized/specialized-pricing-optimizer.md +254 -0
  33. package/data/zh/support/support-recruitment-specialist.md +508 -0
  34. package/lib/catalog.js +3 -3
  35. package/lib/client.js +85 -9
  36. package/lib/index.js +157 -12
  37. package/lib/remote-schemas.js +19 -0
  38. package/lib/remote.js +43 -4
  39. package/lib/teams/state.js +9 -0
  40. package/package.json +2 -2
  41. package/vendor/third-party-licenses/README.md +2 -2
  42. package/data/zh/marketing/marketing-bilibili-content-strategist.md +0 -40
  43. package/data/zh/specialized/specialized-chief-of-staff.md +0 -40
  44. /package/data/experts/{specialized/specialized-chief-of-staff.md → company/chief-of-staff.md} +0 -0
  45. /package/data/experts/engineering/{engineering-china-network-engineer.md → engineering-network-engineer-china.md} +0 -0
  46. /package/data/experts/marketing/{marketing-bilibili-content-strategist.md → marketing-bilibili-strategist.md} +0 -0
  47. /package/data/experts/marketing/{marketing-china-ecommerce-operator.md → marketing-ecommerce-operator.md} +0 -0
  48. /package/data/experts/marketing/{marketing-wechat-official-account.md → marketing-wechat-operator.md} +0 -0
  49. /package/data/experts/marketing/{marketing-xiaohongshu-specialist.md → marketing-xiaohongshu-operator.md} +0 -0
  50. /package/data/experts/{engineering/engineering-prompt-engineer.md → specialized/prompt-engineer.md} +0 -0
  51. /package/data/zh/engineering/{engineering-china-network-engineer.md → engineering-network-engineer-china.md} +0 -0
  52. /package/data/zh/manual-bodies/engineering/{engineering-china-network-engineer.md → engineering-network-engineer-china.md} +0 -0
  53. /package/data/zh/marketing/{marketing-china-ecommerce-operator.md → marketing-ecommerce-operator.md} +0 -0
  54. /package/data/zh/marketing/{marketing-wechat-official-account.md → marketing-wechat-operator.md} +0 -0
  55. /package/data/zh/marketing/{marketing-xiaohongshu-specialist.md → marketing-xiaohongshu-operator.md} +0 -0
  56. /package/data/zh/{engineering/engineering-prompt-engineer.md → specialized/prompt-engineer.md} +0 -0
package/lib/client.js CHANGED
@@ -19925,6 +19925,14 @@ var teamSchema = external_exports.object({
19925
19925
  teamId: external_exports.string(),
19926
19926
  name: external_exports.string(),
19927
19927
  workspace: external_exports.string(),
19928
+ // 2026-09-15:teamId 是团队名 slug 化的结果,**跨工作区会重名**,客户端把 workspacePath
19929
+ // 回传给 stop/deleteTeam 才能精确定位(strict schema 不声明就会被剥掉)。
19930
+ // 两个字段都 `.optional()` 是**兼容闸门**:客户端产物热重载,host 可能是旧的(不返回它们)——
19931
+ // 那时必需字段会让 `teamsSnapshotSchema.parse` 直接失败、列表**整片空白**。可选之后退化为
19932
+ // 「按 halted 判定按钮 + 按 teamId 全量遍历」的旧行为,而不是白屏。
19933
+ workspacePath: external_exports.string().optional(),
19934
+ /** 队长会话此刻是否在运行:客户端据此决定按钮叫「停止」还是「归档」。 */
19935
+ captainActive: external_exports.boolean().optional(),
19928
19936
  captainSessionId: external_exports.string(),
19929
19937
  phase: external_exports.string(),
19930
19938
  halted: external_exports.boolean(),
@@ -19940,7 +19948,16 @@ var startResultSchema = external_exports.object({
19940
19948
  var stopResultSchema = external_exports.object({
19941
19949
  teamName: external_exports.string(),
19942
19950
  cancelledTasks: external_exports.number(),
19943
- alreadyHalted: external_exports.boolean()
19951
+ alreadyHalted: external_exports.boolean(),
19952
+ // 2026-09-15 起,「停止」在队长会话已结束(或团队早已 halted)时改为**归档**:
19953
+ // 这个字段让面板知道该报「已停止」还是「已归档」。
19954
+ archived: external_exports.boolean()
19955
+ });
19956
+ var deleteTeamResultSchema = external_exports.object({
19957
+ teamName: external_exports.string(),
19958
+ deleted: external_exports.boolean(),
19959
+ // 删掉的是归档里那份(而不是还挂在「运行中」的那份)
19960
+ wasArchived: external_exports.boolean()
19944
19961
  });
19945
19962
  var squadInputSchema = external_exports.object({
19946
19963
  key: external_exports.string().min(1).max(64),
@@ -19998,7 +20015,7 @@ var scheduleWorkspaceSchema = external_exports.object({
19998
20015
 
19999
20016
  // src/client.jsx
20000
20017
  var NS = "t-team";
20001
- var CLIENT_BUILD_VERSION = "0.3.24";
20018
+ var CLIENT_BUILD_VERSION = "0.3.26";
20002
20019
  var uiPrefs = { showTaskBadge: true, showScheduleButton: true };
20003
20020
  var uiPrefsListeners = /* @__PURE__ */ new Set();
20004
20021
  function setUiPrefs(next) {
@@ -20191,6 +20208,16 @@ var zh = {
20191
20208
  "teams.stopConfirm": "\u505C\u6B62\u56E2\u961F\u300C{name}\u300D\uFF1F\u672A\u5B8C\u6210\u7684\u4EFB\u52A1\u4F1A\u88AB\u53D6\u6D88\uFF0C\u5DF2\u4EA4\u4ED8\u7ED3\u679C\u4FDD\u7559\u3002",
20192
20209
  "teams.stopping": "\u505C\u6B62\u4E2D\u2026",
20193
20210
  "teams.stopped": "\u5DF2\u505C\u6B62 {name}\uFF08\u53D6\u6D88 {count} \u9879\u4EFB\u52A1\uFF09",
20211
+ // 已停止、或队长会话已结束的团队:按钮改为「归档」——它本来就已经不干活了,
20212
+ // 唯一还想要的动作是把它从「运行中」列表里清掉(任务记录进「看归档」)。
20213
+ "teams.archive": "\u5F52\u6863",
20214
+ "teams.archiveConfirm": "\u5F52\u6863\u56E2\u961F\u300C{name}\u300D\uFF1F\u5B83\u4F1A\u4ECE\u300C\u8FD0\u884C\u4E2D\u300D\u79FB\u5230\u300C\u770B\u5F52\u6863\u300D\uFF0C\u4EFB\u52A1\u8BB0\u5F55\u4FDD\u7559\u3002",
20215
+ "teams.archiving": "\u5F52\u6863\u4E2D\u2026",
20216
+ "teams.archivedNote": "\u5DF2\u5F52\u6863 {name}",
20217
+ "teams.remove": "\u5220\u9664",
20218
+ "teams.removeConfirm": "\u5220\u9664\u56E2\u961F\u300C{name}\u300D\uFF1F\u56E2\u961F\u72B6\u6001\u4E0E\u4EFB\u52A1\u8BB0\u5F55\u4F1A\u88AB\u6C38\u4E45\u5220\u9664\uFF0C\u65E0\u6CD5\u6062\u590D\u3002",
20219
+ "teams.removing": "\u5220\u9664\u4E2D\u2026",
20220
+ "teams.removed": "\u5DF2\u5220\u9664 {name}",
20194
20221
  "teams.refresh": "\u5237\u65B0",
20195
20222
  "teams.tasks": "{done}/{total} \u4EFB\u52A1",
20196
20223
  "teams.members": "{count} \u540D\u6210\u5458",
@@ -20452,6 +20479,14 @@ var en = {
20452
20479
  "teams.stopConfirm": 'Stop team "{name}"? Unfinished tasks are cancelled; delivered results are kept.',
20453
20480
  "teams.stopping": "Stopping\u2026",
20454
20481
  "teams.stopped": "Stopped {name} ({count} tasks cancelled)",
20482
+ "teams.archive": "Archive",
20483
+ "teams.archiveConfirm": 'Archive team "{name}"? It moves from active to archived; task records are kept.',
20484
+ "teams.archiving": "Archiving\u2026",
20485
+ "teams.archivedNote": "Archived {name}",
20486
+ "teams.remove": "Delete",
20487
+ "teams.removeConfirm": 'Delete team "{name}"? Its state and task records are permanently removed; this cannot be undone.',
20488
+ "teams.removing": "Deleting\u2026",
20489
+ "teams.removed": "Deleted {name}",
20455
20490
  "teams.refresh": "Refresh",
20456
20491
  "teams.tasks": "{done}/{total} tasks",
20457
20492
  "teams.members": "{count} members",
@@ -20645,6 +20680,9 @@ var CSS = `
20645
20680
  .t-team-chip:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
20646
20681
  .t-team-chip:disabled{opacity:.5;cursor:default}
20647
20682
  .t-team-chip[data-active="true"]{background:var(--dsw-alias-button-ghost-active-fill);border-color:var(--dsw-alias-button-ghost-active-border);color:var(--dsw-alias-label-primary);font-weight:600}
20683
+ /* \u7834\u574F\u6027\u52A8\u4F5C\uFF08\u5220\u9664\u56E2\u961F\uFF09\uFF1A\u5E38\u6001\u5C31\u5E26\u4E0A\u9519\u8BEF\u8272\uFF0C\u522B\u7B49 hover \u624D\u63D0\u793A\u3002 */
20684
+ .t-team-chip[data-tone="error"]{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb,var(--dsw-alias-state-error-primary) 40%,transparent)}
20685
+ .t-team-chip[data-tone="error"]:hover{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 12%,transparent);color:var(--dsw-alias-state-error-primary)}
20648
20686
  .t-team-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:10px 12px}
20649
20687
  .t-team-group{margin-top:6px}
20650
20688
  .t-team-division{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary);margin:2px 0 6px}
@@ -20915,9 +20953,16 @@ var TYPERT_REMOTE = {
20915
20953
  direct("getTeams", [
20916
20954
  { name: "archived", wire: "archived", source: "json", codec: { mode: "strict", typeSymbol: "boolean", schema: external_exports.boolean() } }
20917
20955
  ], "TTeamTeams", teamsSnapshotSchema),
20956
+ // workspacePath 标 acceptsUndefined:host 若是旧版(快照里没这个字段),这里就是 undefined,
20957
+ // 请求里不带该键,宿主退回全量遍历;不带标记会在 gateway 侧被判 arguments-invalid。
20918
20958
  direct("stopTeam", [
20919
- { name: "teamId", wire: "teamId", source: "json", codec: { mode: "strict", typeSymbol: "string", schema: external_exports.string().min(1).max(128) } }
20959
+ { name: "teamId", wire: "teamId", source: "json", codec: { mode: "strict", typeSymbol: "string", schema: external_exports.string().min(1).max(128) } },
20960
+ { name: "workspacePath", wire: "workspacePath", source: "json", acceptsUndefined: true, codec: { mode: "strict", typeSymbol: "string", schema: external_exports.string().max(1024) } }
20920
20961
  ], "TTeamStopResult", stopResultSchema),
20962
+ direct("deleteTeam", [
20963
+ { name: "teamId", wire: "teamId", source: "json", codec: { mode: "strict", typeSymbol: "string", schema: external_exports.string().min(1).max(128) } },
20964
+ { name: "workspacePath", wire: "workspacePath", source: "json", acceptsUndefined: true, codec: { mode: "strict", typeSymbol: "string", schema: external_exports.string().max(1024) } }
20965
+ ], "TTeamDeleteResult", deleteTeamResultSchema),
20921
20966
  direct("createExpert", [
20922
20967
  { name: "expert", wire: "expert", source: "json", codec: { mode: "strict", typeSymbol: "TTeamCustomExpertInput", schema: customExpertInputSchema } }
20923
20968
  ], "TTeamCatalog", catalogSnapshotSchema),
@@ -22109,7 +22154,7 @@ function SettingsPanel({ t, ctx, remote, getActive, refreshCatalog, initialTab,
22109
22154
  onClick: () => void setShowScheduleButton(!showScheduleButton)
22110
22155
  },
22111
22156
  /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-switch-knob" })
22112
- )), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-setting-hint" }, t("settings.showScheduleButtonHint"))), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-tabs", role: "tablist" }, [["experts", "tab.experts"], ["categories", "tab.categories"], ["roster", "tab.roster"]].map(([key, label]) => /* @__PURE__ */ import_react.default.createElement(
22157
+ )), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-setting-hint" }, t("settings.showScheduleButtonHint"))), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-tabs", role: "tablist" }, [["experts", "tab.experts"], ["categories", "tab.categories"], ["roster", "tab.roster"], ["teams", "tab.teams"]].map(([key, label]) => /* @__PURE__ */ import_react.default.createElement(
22113
22158
  "button",
22114
22159
  {
22115
22160
  className: "t-team-tab",
@@ -22121,7 +22166,7 @@ function SettingsPanel({ t, ctx, remote, getActive, refreshCatalog, initialTab,
22121
22166
  onClick: () => setTab(key)
22122
22167
  },
22123
22168
  t(label)
22124
- ))), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-tabbody", hidden: tab !== "categories" }, /* @__PURE__ */ import_react.default.createElement(SectionBoundary, { t, reloadKey: tab }, /* @__PURE__ */ import_react.default.createElement(CategoriesTab, { t, remote, categories, onChanged: () => reload(true) }))), tab === "roster" && /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-tabbody" }, /* @__PURE__ */ import_react.default.createElement(SectionBoundary, { t, reloadKey: tab }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-section-head" }, t("roster.squads")), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-note" }, t("roster.squadsHint"))), /* @__PURE__ */ import_react.default.createElement(SquadsTab, { t, remote, initialData: squadsPreview, initialDraft: draftPreview }), /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-section-head" }, t("roster.teams")), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-note" }, t("roster.teamsHint"))), /* @__PURE__ */ import_react.default.createElement(TeamsTab, { t, remote, initialTeams: teamsPreview }))), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-tabbody", hidden: tab !== "experts" }, /* @__PURE__ */ import_react.default.createElement(SectionBoundary, { t, reloadKey: tab }, /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-note" }, t("roster.expertsHint")), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-bar" }, /* @__PURE__ */ import_react.default.createElement(
22169
+ ))), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-tabbody", hidden: tab !== "categories" }, /* @__PURE__ */ import_react.default.createElement(SectionBoundary, { t, reloadKey: tab }, /* @__PURE__ */ import_react.default.createElement(CategoriesTab, { t, remote, categories, onChanged: () => reload(true) }))), tab === "roster" && /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-tabbody" }, /* @__PURE__ */ import_react.default.createElement(SectionBoundary, { t, reloadKey: tab }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-section-head" }, t("roster.squads")), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-note" }, t("roster.squadsHint"))), /* @__PURE__ */ import_react.default.createElement(SquadsTab, { t, remote, initialData: squadsPreview, initialDraft: draftPreview }))), tab === "teams" && /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-tabbody" }, /* @__PURE__ */ import_react.default.createElement(SectionBoundary, { t, reloadKey: tab }, /* @__PURE__ */ import_react.default.createElement("div", null, /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-section-head" }, t("roster.teams")), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-note" }, t("roster.teamsHint"))), /* @__PURE__ */ import_react.default.createElement(TeamsTab, { t, remote, initialTeams: teamsPreview }))), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-tabbody", hidden: tab !== "experts" }, /* @__PURE__ */ import_react.default.createElement(SectionBoundary, { t, reloadKey: tab }, /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-note" }, t("roster.expertsHint")), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-bar" }, /* @__PURE__ */ import_react.default.createElement(
22125
22170
  "input",
22126
22171
  {
22127
22172
  className: "t-team-input",
@@ -23028,6 +23073,7 @@ function TeamsTab({ t, remote, initialTeams, compact }) {
23028
23073
  const [state, setState] = import_react.default.useState({ teams: initialTeams ?? null, error: "" });
23029
23074
  const [archived, setArchived] = import_react.default.useState(false);
23030
23075
  const [busyId, setBusyId] = import_react.default.useState("");
23076
+ const [removingId, setRemovingId] = import_react.default.useState("");
23031
23077
  const [note, setNote] = import_react.default.useState("");
23032
23078
  const load = import_react.default.useCallback(async (wantArchived) => {
23033
23079
  setState((current) => ({ teams: current.teams, error: "" }));
@@ -23050,12 +23096,13 @@ function TeamsTab({ t, remote, initialTeams, compact }) {
23050
23096
  }));
23051
23097
  }
23052
23098
  async function stop(team) {
23053
- if (!window.confirm(t("teams.stopConfirm", { name: team.name }))) return;
23099
+ const archiving = team.halted === true || team.captainActive === false;
23100
+ if (!window.confirm(t(archiving ? "teams.archiveConfirm" : "teams.stopConfirm", { name: team.name }))) return;
23054
23101
  setBusyId(team.teamId);
23055
23102
  setNote("");
23056
23103
  try {
23057
- const result = unwrap(await remote.stopTeam(team.teamId), "stopTeam");
23058
- setNote(t("teams.stopped", { name: result?.teamName ?? team.name, count: result?.cancelledTasks ?? 0 }));
23104
+ const result = unwrap(await remote.stopTeam(team.teamId, team.workspacePath), "stopTeam");
23105
+ setNote(result?.archived === true ? t("teams.archivedNote", { name: result?.teamName ?? team.name }) : t("teams.stopped", { name: result?.teamName ?? team.name, count: result?.cancelledTasks ?? 0 }));
23059
23106
  await load(archived);
23060
23107
  } catch (cause) {
23061
23108
  setState({ teams: state.teams, error: cause instanceof Error ? cause.message : String(cause) });
@@ -23063,14 +23110,43 @@ function TeamsTab({ t, remote, initialTeams, compact }) {
23063
23110
  setBusyId("");
23064
23111
  }
23065
23112
  }
23113
+ async function remove(team) {
23114
+ if (!window.confirm(t("teams.removeConfirm", { name: team.name }))) return;
23115
+ setRemovingId(team.teamId);
23116
+ setNote("");
23117
+ try {
23118
+ const result = unwrap(await remote.deleteTeam(team.teamId, team.workspacePath), "deleteTeam");
23119
+ setNote(t("teams.removed", { name: result?.teamName ?? team.name }));
23120
+ await load(archived);
23121
+ } catch (cause) {
23122
+ setState({ teams: state.teams, error: cause instanceof Error ? cause.message : String(cause) });
23123
+ } finally {
23124
+ setRemovingId("");
23125
+ }
23126
+ }
23066
23127
  const teams = state.teams;
23067
23128
  return /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-tabbody" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-row" }, compact !== true && /* @__PURE__ */ import_react.default.createElement("button", { className: "t-team-chip", type: "button", onClick: () => setArchived((value) => !value) }, archived ? t("teams.showActive") : t("teams.showArchived")), /* @__PURE__ */ import_react.default.createElement("button", { className: "t-team-chip", type: "button", onClick: () => void load(archived) }, t("teams.refresh")), /* @__PURE__ */ import_react.default.createElement("button", { className: "t-team-chip", type: "button", onClick: () => openPanel((teams ?? [])[0]) }, t("teams.openPanel"))), note !== "" && /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-ok" }, note), state.error !== "" && /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-error" }, state.error), teams === null && /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-meta" }, t("teams.loading")), teams !== null && teams.length === 0 && /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-meta" }, archived ? t("teams.emptyArchived") : t("teams.empty")), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-teams" }, (teams ?? []).map((team) => {
23068
23129
  const taskList = Array.isArray(team?.tasks) ? team.tasks : [];
23069
23130
  const memberList = Array.isArray(team?.members) ? team.members : [];
23070
23131
  const done = taskList.filter((task) => task?.status === "completed").length;
23071
23132
  const working = memberList.filter((member) => member?.activity === "working").length;
23133
+ const archivable = team.halted === true || team.captainActive === false;
23072
23134
  const phase = team.halted === true ? t("teams.halted") : archived ? t("teams.archived") : team.phase === "staged" ? t("teams.staged") : t("teams.running");
23073
- return /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-team", key: team.teamId }, /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-team-head" }, /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-team-name", title: `${team.name} \xB7 ${team.teamId}` }, team.name), /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-badge", "data-tone": team.halted === true ? "error" : team.phase === "staged" ? "warn" : void 0 }, phase)), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-team-meta" }, /* @__PURE__ */ import_react.default.createElement("span", null, t("teams.members", { count: memberList.length })), /* @__PURE__ */ import_react.default.createElement("span", null, working > 0 ? `${t("teams.working", { count: working })} \xB7 ` : "", t("teams.tasks", { done, total: taskList.length })), /* @__PURE__ */ import_react.default.createElement("span", null, team.workspace)), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-row" }, memberList.slice(0, 8).map((member) => /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-member-chip", key: member.name, "data-unknown": member.activity === "unknown" }, member.name))), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-row" }, /* @__PURE__ */ import_react.default.createElement("button", { className: "t-team-chip", type: "button", onClick: () => openPanel(team) }, t("teams.openPanel")), !archived && /* @__PURE__ */ import_react.default.createElement("button", { className: "t-team-chip", type: "button", disabled: busyId === team.teamId, onClick: () => void stop(team) }, busyId === team.teamId ? t("teams.stopping") : t("teams.stop"))));
23135
+ return (
23136
+ // key 必须带上工作区:teamId 跨工作区会重名,只用 teamId 会让两行共用同一个 key
23137
+ // (同列表内渲染状态互相串)。
23138
+ /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-team", key: `${team.workspacePath ?? ""}:${team.teamId}` }, /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-team-head" }, /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-team-name", title: `${team.name} \xB7 ${team.teamId}` }, team.name), /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-badge", "data-tone": team.halted === true ? "error" : team.phase === "staged" ? "warn" : void 0 }, phase)), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-team-meta" }, /* @__PURE__ */ import_react.default.createElement("span", null, t("teams.members", { count: memberList.length })), /* @__PURE__ */ import_react.default.createElement("span", null, working > 0 ? `${t("teams.working", { count: working })} \xB7 ` : "", t("teams.tasks", { done, total: taskList.length })), /* @__PURE__ */ import_react.default.createElement("span", null, team.workspace)), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-row" }, memberList.slice(0, 8).map((member) => /* @__PURE__ */ import_react.default.createElement("span", { className: "t-team-member-chip", key: member.name, "data-unknown": member.activity === "unknown" }, member.name))), /* @__PURE__ */ import_react.default.createElement("div", { className: "t-team-row" }, /* @__PURE__ */ import_react.default.createElement("button", { className: "t-team-chip", type: "button", onClick: () => openPanel(team) }, t("teams.openPanel")), !archived && /* @__PURE__ */ import_react.default.createElement("button", { className: "t-team-chip", type: "button", disabled: busyId === team.teamId || removingId === team.teamId, onClick: () => void stop(team) }, busyId === team.teamId ? archivable ? t("teams.archiving") : t("teams.stopping") : archivable ? t("teams.archive") : t("teams.stop")), /* @__PURE__ */ import_react.default.createElement(
23139
+ "button",
23140
+ {
23141
+ className: "t-team-chip",
23142
+ type: "button",
23143
+ "data-tone": "error",
23144
+ disabled: removingId === team.teamId || busyId === team.teamId,
23145
+ onClick: () => void remove(team)
23146
+ },
23147
+ removingId === team.teamId ? t("teams.removing") : t("teams.remove")
23148
+ )))
23149
+ );
23074
23150
  })));
23075
23151
  }
23076
23152
  function resolveInputTarget(ctx, sessionId) {
package/lib/index.js CHANGED
@@ -49,7 +49,14 @@ import { checkPlan } from "./plan-check.js";
49
49
  // 定时事项引擎:T专家 自己的实现(JSON 文件 + croner),与 @weibaohui/dsh-tasks 零共享 ——
50
50
  // 两个插件允许同时安装同时可用,所以存储、服务名、错误码、消息来源全都各走各的。
51
51
  import { SCHEDULE_FILE_NAME, SCHEDULE_SERVICE, createScheduleEngine } from "./schedule.js";
52
- import { findTeamByCaptain } from "./teams/state.js";
52
+ import {
53
+ archiveTeamDir,
54
+ findTeamByCaptain,
55
+ listArchivedTeamIds,
56
+ readArchivedTeam,
57
+ removeArchivedTeamDir,
58
+ removeTeamDir,
59
+ } from "./teams/state.js";
53
60
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
54
61
  // 内置团队引擎:随插件一起发布的品牌化产物在 lib/teams/ 下(源自第三方引擎,改名后
55
62
  // 由本项目直接维护),这里在 T专家 内部挂载它 ——
@@ -1459,10 +1466,51 @@ export function apply(ctx, config) {
1459
1466
  }))
1460
1467
  .filter((root) => typeof root.stateRoot === "string");
1461
1468
  };
1462
- const compactTeam = (team) => ({
1469
+ /**
1470
+ * 团队 id 的格式闸门:这个值会被直接拼进文件路径(`<stateRoot>/<teamId>` 与
1471
+ * `<stateRoot>/archive/<teamId>`),所以要挡住的是**路径穿越**,而不是"必须长得像 slug"。
1472
+ *
1473
+ * 2026-09-15 二次修正(复核 N2):第一版要求 `sanitizeKey(teamId) === teamId`,结果是盘上
1474
+ * 任何**非 slug 目录名**的团队(历史遗留、手工建的目录,如 `My Team`)在设置页里变成
1475
+ * "按钮点得动、点了必失败"的僵尸行 —— 而它在加闸门之前是删得掉的。现在按"能不能安全拼进
1476
+ * 路径"判定:不接受分隔符、`..`、NUL,也不接受带目录成分的路径。
1477
+ */
1478
+ const assertTeamId = (teamId) => {
1479
+ const bad = typeof teamId !== "string"
1480
+ || teamId === "" || teamId === "." || teamId === ".."
1481
+ || teamId.includes("/") || teamId.includes("\\") || teamId.includes("\0")
1482
+ || basename(teamId) !== teamId;
1483
+ if (bad) {
1484
+ throw new Error(`团队 id 非法:${JSON.stringify(teamId)}(不能为空、不能含路径分隔符或 "..")。`);
1485
+ }
1486
+ };
1487
+ /**
1488
+ * 定位要操作的 stateRoot:给了 `workspacePath` 就**只用它**。
1489
+ *
1490
+ * 2026-09-15(独立审查发现的缺陷):`teamId` 是团队名 `sanitizeKey(name)` 的结果,
1491
+ * 不同工作区里的同名团队会拿到同一个 teamId —— 只按 teamId 找,`find` 会命中最靠前的那个
1492
+ * 工作区,于是「点 B 工作区那支的删除」实际删掉 A 工作区的目录(停止同理)。
1493
+ *
1494
+ * 空串与 undefined 都**放行**(退回全量遍历):那是「客户端比 host 旧、快照里没带
1495
+ * workspacePath」的降级路径 —— 此时除全遍历外无从定位,而它带着 F1 那个固有歧义。
1496
+ * 这是旧版本组合下唯一可能的降级,不是新客户端会走的路径。
1497
+ */
1498
+ const locateRoots = (roots, workspacePath) => {
1499
+ if (typeof workspacePath !== "string" || workspacePath === "") return roots;
1500
+ const hit = roots.find((root) => root.path === workspacePath);
1501
+ if (hit === undefined) {
1502
+ throw new Error(`工作区不在当前列表里:${workspacePath}(它可能已被移除,或不在本机工作区注册表里)。`);
1503
+ }
1504
+ return [hit];
1505
+ };
1506
+ const compactTeam = (team, workspacePath, captainActive) => ({
1463
1507
  teamId: team.teamId,
1464
1508
  name: team.name,
1465
1509
  workspace: team.workspace,
1510
+ /** 所属工作区路径:同名团队跨工作区时,客户端靠它精确定位(见 locateRoots)。 */
1511
+ workspacePath,
1512
+ /** 队长会话此刻是否在运行:客户端据此决定按钮叫「停止」还是「归档」,与服务端判定对齐。 */
1513
+ captainActive,
1466
1514
  // 面板/设置页的「打开活动面板」要带 captainSessionId:活动面板是会话作用域的,
1467
1515
  // 缺它就只能匹配当前会话的团队,别的会话(或历史归档)点按钮会静默无反应。
1468
1516
  captainSessionId: team.captainSessionId,
@@ -1487,10 +1535,33 @@ export function apply(ctx, config) {
1487
1535
  async list(archived = false) {
1488
1536
  const roots = teamRoots();
1489
1537
  if (roots.length === 0) return [];
1490
- const snapshots = archived
1491
- ? await collectArchivedTeamsActivity(ctx, roots)
1492
- : await collectTeamsActivity(ctx, roots);
1493
- return snapshots.map(compactTeam);
1538
+ // 逐工作区收集(而不是一次性把 roots 交给 snapshot 层):只有这样才能把**每支团队**
1539
+ // 与它所属的工作区路径、以及「队长此刻是否在运行」一起带出去(见 compactTeam 注释)。
1540
+ const agents = ctx.get("agents");
1541
+ const out = [];
1542
+ const failures = [];
1543
+ for (const root of roots) {
1544
+ let snapshots;
1545
+ try {
1546
+ snapshots = archived
1547
+ ? await collectArchivedTeamsActivity(ctx, [root])
1548
+ : await collectTeamsActivity(ctx, [root]);
1549
+ } catch (error) {
1550
+ const reason = error instanceof Error ? error.message : String(error);
1551
+ failures.push(`${root.stateRoot}:${reason}`);
1552
+ ctx.logger?.warn?.(`[t-team] 读取团队活动失败:${root.stateRoot}(${reason})`);
1553
+ continue;
1554
+ }
1555
+ for (const snapshot of snapshots) {
1556
+ out.push(compactTeam(snapshot, root.path, agents?.get?.(snapshot.captainSessionId) !== undefined));
1557
+ }
1558
+ }
1559
+ // 全部工作区都读失败时不要返回空数组 —— UI 会把它当「没有团队」显示,
1560
+ // 真实原因(EACCES 之类)就此消失(与 stop/remove 里的 N-4 同一条原则)。
1561
+ if (out.length === 0 && failures.length > 0 && failures.length === roots.length) {
1562
+ throw new Error(`读取团队活动失败:${roots.length} 个工作区全部读取失败(${failures.join(";")})。`);
1563
+ }
1564
+ return out;
1494
1565
  },
1495
1566
  /** 面板里点一支小队:投一条 `/t --profile <key>` 让队长按该小队接活(目标由队长追问用户)。 */
1496
1567
  async start(profileKey, sessionId) {
@@ -1512,8 +1583,9 @@ export function apply(ctx, config) {
1512
1583
  members: hit.members.length,
1513
1584
  };
1514
1585
  },
1515
- async stop(teamId) {
1516
- const roots = teamRoots();
1586
+ async stop(teamId, workspacePath) {
1587
+ assertTeamId(teamId);
1588
+ const roots = locateRoots(teamRoots(), workspacePath);
1517
1589
  // 逐 root 读取失败时**留住根因**:过去 `catch { continue; }` 把 EACCES 之类换成
1518
1590
  // 下面那句「没找到团队 …(它可能已被归档)」,用户按提示去查归档永远查不到(N-4)。
1519
1591
  const failures = [];
@@ -1529,18 +1601,91 @@ export function apply(ctx, config) {
1529
1601
  }
1530
1602
  const team = snapshots.find((item) => item.teamId === teamId);
1531
1603
  if (team === undefined) continue;
1604
+ // 2026-09-15 修(用户报「运行中的团队删不掉」):过去只要队长会话不在运行就直接抛错,
1605
+ // 于是 DSH 重启后残留下来的团队在设置页里**永远**清不掉 —— 它躺在「运行中」列表里,
1606
+ // 点一次报一次「队长会话已不在运行中」,而那句提示让用户去活动面板归档,活动面板是
1607
+ // 会话作用域的、队长会话都没了自然也打不开。现在按状态分流:
1608
+ // · 队长在运行且还没停 → haltTeamWork(原行为:取消未完成任务 + 中断成员)
1609
+ // · 队长不在运行、或团队早已 halted → **归档**(目录移进 <stateRoot>/archive/,
1610
+ // 从「运行中」列表消失,「看归档」里仍能查到它的任务记录)
1532
1611
  const captain = ctx.get("agents")?.get?.(team.captainSessionId);
1533
- if (captain === undefined) {
1534
- throw new Error("队长会话已不在运行中:这类团队只能用 /t 恢复后再操作,或直接在活动面板里归档。");
1612
+ if (captain !== undefined && team.halted !== true) {
1613
+ const result = await haltTeamWork({ ctx, stateRoot: root.stateRoot, teamId, captain });
1614
+ return {
1615
+ teamName: result.teamName,
1616
+ cancelledTasks: result.cancelledTasks,
1617
+ alreadyHalted: result.alreadyHalted === true,
1618
+ archived: false,
1619
+ };
1535
1620
  }
1536
- const result = await haltTeamWork({ ctx, stateRoot: root.stateRoot, teamId, captain });
1537
- return { teamName: result.teamName, cancelledTasks: result.cancelledTasks, alreadyHalted: result.alreadyHalted === true };
1621
+ await archiveTeamDir(root.stateRoot, teamId);
1622
+ ctx.logger?.info?.(`[t-team] 团队「${team.name}」已归档(队长不在运行:${String(captain === undefined)})。`);
1623
+ return {
1624
+ teamName: team.name,
1625
+ cancelledTasks: 0,
1626
+ alreadyHalted: team.halted === true,
1627
+ archived: true,
1628
+ };
1538
1629
  }
1539
1630
  // 读不到任何 root 时不要用「可能已归档」这种猜测掩盖根因:把真实失败原因一并报出来。
1540
1631
  throw new Error(failures.length > 0
1541
1632
  ? `没找到团队 ${teamId}:${roots.length} 个工作区里${failures.length} 个读取失败(${failures.join(";")})。`
1542
1633
  : `没找到团队 ${teamId} 的状态目录(它可能已被归档)。`);
1543
1634
  },
1635
+ /**
1636
+ * 彻底删除一支团队(运行中的与已归档的都收)—— 设置页那一行上的「删除」按钮。
1637
+ *
1638
+ * 2026-09-15 加:用户报「已经停止的团队没有删除按钮」。stop 只能让它停下/进归档,
1639
+ * 目录与任务记录都还在;要真正清掉就得删目录。运行中的先照常停一次(取消未完成任务、
1640
+ * 中断成员),避免留下还在跑的成员;队长不在运行就直接删(成员随会话结束已经没了)。
1641
+ */
1642
+ async remove(teamId, workspacePath) {
1643
+ assertTeamId(teamId);
1644
+ const roots = locateRoots(teamRoots(), workspacePath);
1645
+ const failures = [];
1646
+ for (const root of roots) {
1647
+ let snapshots;
1648
+ try {
1649
+ snapshots = await collectTeamsActivity(ctx, [root]);
1650
+ } catch (error) {
1651
+ const reason = error instanceof Error ? `${error.message}` : String(error);
1652
+ failures.push(`${root.stateRoot}:${reason}`);
1653
+ ctx.logger?.warn?.(`[t-team] 读取团队活动失败:${root.stateRoot}(${reason})`);
1654
+ continue;
1655
+ }
1656
+ const team = snapshots.find((item) => item.teamId === teamId);
1657
+ if (team !== undefined) {
1658
+ const captain = ctx.get("agents")?.get?.(team.captainSessionId);
1659
+ if (captain !== undefined && team.halted !== true) {
1660
+ try {
1661
+ await haltTeamWork({ ctx, stateRoot: root.stateRoot, teamId, captain });
1662
+ } catch (error) {
1663
+ // 停不下来不该挡住删除:目录都要删了,成员随会话结束时也会被回收。
1664
+ const reason = error instanceof Error ? error.message : String(error);
1665
+ ctx.logger?.warn?.(`[t-team] 删除前停止团队「${team.name}」失败(继续删除):${reason}`);
1666
+ }
1667
+ }
1668
+ await removeTeamDir(root.stateRoot, teamId);
1669
+ // 防护(独立审查 F3):队长/成员若还有在途写盘,`writeTeam` 会 `mkdir -p` 把目录
1670
+ // **重建**出来(state.js 里那行是无条件的),于是「删掉的团队」过一会儿又冒出来。
1671
+ // 这里确认一次:真被重建就再删一次并出声 —— 静默留下空壳比报错更难查。
1672
+ if (existsSync(join(root.stateRoot, teamId))) {
1673
+ ctx.logger?.warn?.(`[t-team] 团队「${team.name}」删除后有写入者重建了目录,已再次删除(可能有成员在收尾)。`);
1674
+ await removeTeamDir(root.stateRoot, teamId);
1675
+ }
1676
+ return { teamName: team.name, deleted: true, wasArchived: false };
1677
+ }
1678
+ const archivedIds = await listArchivedTeamIds(root.stateRoot);
1679
+ if (archivedIds.includes(teamId)) {
1680
+ const state = await readArchivedTeam(root.stateRoot, teamId);
1681
+ await removeArchivedTeamDir(root.stateRoot, teamId);
1682
+ return { teamName: state?.name ?? teamId, deleted: true, wasArchived: true };
1683
+ }
1684
+ }
1685
+ throw new Error(failures.length > 0
1686
+ ? `没找到团队 ${teamId}:${roots.length} 个工作区里${failures.length} 个读取失败(${failures.join(";")})。`
1687
+ : `没找到团队 ${teamId} 的状态目录(它可能已被删除)。`);
1688
+ },
1544
1689
  });
1545
1690
 
1546
1691
  // ---- `/t` 小队短命令(把 /t <小队> <目标> 转成引擎认得的 /t --profile <key> <目标> 用户消息)----
@@ -141,6 +141,14 @@ export const teamSchema = z.object({
141
141
  teamId: z.string(),
142
142
  name: z.string(),
143
143
  workspace: z.string(),
144
+ // 2026-09-15:teamId 是团队名 slug 化的结果,**跨工作区会重名**,客户端把 workspacePath
145
+ // 回传给 stop/deleteTeam 才能精确定位(strict schema 不声明就会被剥掉)。
146
+ // 两个字段都 `.optional()` 是**兼容闸门**:客户端产物热重载,host 可能是旧的(不返回它们)——
147
+ // 那时必需字段会让 `teamsSnapshotSchema.parse` 直接失败、列表**整片空白**。可选之后退化为
148
+ // 「按 halted 判定按钮 + 按 teamId 全量遍历」的旧行为,而不是白屏。
149
+ workspacePath: z.string().optional(),
150
+ /** 队长会话此刻是否在运行:客户端据此决定按钮叫「停止」还是「归档」。 */
151
+ captainActive: z.boolean().optional(),
144
152
  captainSessionId: z.string(),
145
153
  phase: z.string(),
146
154
  halted: z.boolean(),
@@ -160,6 +168,17 @@ export const stopResultSchema = z.object({
160
168
  teamName: z.string(),
161
169
  cancelledTasks: z.number(),
162
170
  alreadyHalted: z.boolean(),
171
+ // 2026-09-15 起,「停止」在队长会话已结束(或团队早已 halted)时改为**归档**:
172
+ // 这个字段让面板知道该报「已停止」还是「已归档」。
173
+ archived: z.boolean(),
174
+ });
175
+
176
+ /** 「删除团队」的结果:与「停止/归档」不同,这是不可逆的清理。 */
177
+ export const deleteTeamResultSchema = z.object({
178
+ teamName: z.string(),
179
+ deleted: z.boolean(),
180
+ // 删掉的是归档里那份(而不是还挂在「运行中」的那份)
181
+ wasArchived: z.boolean(),
163
182
  });
164
183
 
165
184
  /** 面板提交的小队定义(saveSquads 的入参)。 */
package/lib/remote.js CHANGED
@@ -40,6 +40,7 @@ import {
40
40
  squadsSnapshotSchema,
41
41
  startResultSchema,
42
42
  stopResultSchema,
43
+ deleteTeamResultSchema,
43
44
  teamMemberSchema,
44
45
  teamSchema,
45
46
  teamsSnapshotSchema,
@@ -195,6 +196,12 @@ const DESCRIPTORS = [
195
196
  codec: { mode: "strict", typeSymbol: "boolean", schema: z.boolean() },
196
197
  },
197
198
  ], "TTeamTeams", teamsSnapshotSchema),
199
+ // teamId 是**团队名 slug 化**的产物,跨工作区会重名;workspacePath 让宿主精确定位到
200
+ // 「用户点的那一行」所在的工作区。
201
+ // `acceptsUndefined: true` 是**兼容闸门**:DSH 的客户端产物是热重载的,host 更新后浏览器
202
+ // 可能还挂着上一版 client bundle —— 那时它只会传 teamId。没有这个标记,gateway 会直接
203
+ // 判 `arguments-invalid: missing "workspacePath"`,用户点「停止/删除」立刻炸;
204
+ // 标上之后旧客户端照旧能调,宿主退回「全量遍历」的旧行为。
198
205
  descriptor("stopTeam", [
199
206
  {
200
207
  name: "teamId",
@@ -202,7 +209,30 @@ const DESCRIPTORS = [
202
209
  source: "json",
203
210
  codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128) },
204
211
  },
212
+ {
213
+ name: "workspacePath",
214
+ wire: "workspacePath",
215
+ source: "json",
216
+ acceptsUndefined: true,
217
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(1024) },
218
+ },
205
219
  ], "TTeamStopResult", stopResultSchema),
220
+ // 彻底删除一支团队(运行中的与已归档的都收):设置页「运行中的团队」那一行的「删除」按钮。
221
+ descriptor("deleteTeam", [
222
+ {
223
+ name: "teamId",
224
+ wire: "teamId",
225
+ source: "json",
226
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128) },
227
+ },
228
+ {
229
+ name: "workspacePath",
230
+ wire: "workspacePath",
231
+ source: "json",
232
+ acceptsUndefined: true,
233
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(1024) },
234
+ },
235
+ ], "TTeamDeleteResult", deleteTeamResultSchema),
206
236
  // 自建专家的增删改:都回传刷新后的整份名册快照,面板拿到就能直接重绘。
207
237
  descriptor("createExpert", [
208
238
  {
@@ -562,15 +592,24 @@ class TTeamRemote extends TypertRemoteService {
562
592
  }
563
593
  }
564
594
 
565
- /** 停止一个团队(等价于活动面板里的停止)。 */
566
- async stopTeam(teamId) {
595
+ /** 停止一个团队(等价于活动面板里的停止)。`workspacePath` 用于同名团队的精确定位。 */
596
+ async stopTeam(teamId, workspacePath = "") {
567
597
  try {
568
- return await this.teams().stop(teamId);
598
+ return await this.teams().stop(teamId, workspacePath);
569
599
  } catch (error) {
570
600
  throw this.businessError(error, "tTeam/stop-failed");
571
601
  }
572
602
  }
573
603
 
604
+ /** 彻底删除一个团队(运行中的先停再删;已归档的直接删)。不可逆。 */
605
+ async deleteTeam(teamId, workspacePath = "") {
606
+ try {
607
+ return await this.teams().remove(teamId, workspacePath);
608
+ } catch (error) {
609
+ throw this.businessError(error, "tTeam/delete-failed");
610
+ }
611
+ }
612
+
574
613
  /**
575
614
  * 业务错误统一转 RemoteError(裸 Error 过线会退化成 gateway/internal 并丢掉 code)。
576
615
  *
@@ -658,7 +697,7 @@ class TTeamRemote extends TypertRemoteService {
658
697
  }
659
698
 
660
699
  exposeRemoteMethods(TTeamRemote, [
661
- "getCatalog", "setEnabled", "getPrompt", "getSquads", "saveSquads", "getTeams", "stopTeam", "startSquad",
700
+ "getCatalog", "setEnabled", "getPrompt", "getSquads", "saveSquads", "getTeams", "stopTeam", "deleteTeam", "startSquad",
662
701
  "getUiPrefs", "setUiPrefs",
663
702
  "getSchedule", "createScheduleItem", "updateScheduleItem", "deleteScheduleItem", "runScheduleItem", "previewSchedule", "ensureScheduleWorkspace",
664
703
  "createExpert", "updateExpert", "deleteExpert",
@@ -855,6 +855,15 @@ export async function archiveTeamDir(stateRoot, teamId) {
855
855
  if (displaced)
856
856
  await rm(previous, { recursive: true, force: true }).catch(() => undefined);
857
857
  }
858
+ /**
859
+ * Remove an archived team's directory — the counterpart of {@link archiveTeamDir}
860
+ * for the "delete it for good" action. Irreversible: the task record goes with it.
861
+ * @param stateRoot - resolved absolute state root directory.
862
+ * @param teamId - the team id.
863
+ */
864
+ export async function removeArchivedTeamDir(stateRoot, teamId) {
865
+ await rm(join(stateRoot, 'archive', teamId), { recursive: true, force: true });
866
+ }
858
867
  /**
859
868
  * Read one archived team (already moved under `archive/`), or undefined when
860
869
  * it was never archived.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-plugin-t-expert",
3
- "version": "0.3.24",
3
+ "version": "0.3.26",
4
4
  "type": "module",
5
- "description": "T Expert — a 316-expert, 22-division roster with full Chinese translations, as a standalone DeepSeek Harness plugin, with a built-in multi-agent team engine (T Team).",
5
+ "description": "T Expert — a 323-expert, 22-division roster with full Chinese translations, as a standalone DeepSeek Harness plugin, with a built-in multi-agent team engine (T Team).",
6
6
  "license": "MIT",
7
7
  "author": "jiuaiwo",
8
8
  "keywords": [
@@ -5,8 +5,8 @@
5
5
 
6
6
  | 文件 | 覆盖的随包内容 | 来源 |
7
7
  | --- | --- | --- |
8
- | `agency-agents.LICENSE` | `data/experts/`(22 分区 / 316 位;其中 314 位来自第三方 —— 279 位为上游仓库逐字节镜像、35 位来自名册来源包快照;余 2 位为本仓自建,不由本许可覆盖) | The Agency / AgentLand 名册快照,MIT |
9
- | `agency-agents-zh.LICENSE` | `data/zh/` 中的中文名字、简介与人格正文 | `agency-agents-zh` 中文翻译与本地化资产,MIT |
8
+ | `agency-agents.LICENSE` | `data/experts/`(22 分区 / 323 位;其中 321 位来自第三方 —— 279 位为上游仓库逐字节镜像、35 位来自名册来源包快照、7 位于 2026-09-15 与 `agency-agents-zh` 源仓库对齐时补齐;余 2 位为本仓自建,不由本许可覆盖) | The Agency / AgentLand 名册快照,MIT |
9
+ | `agency-agents-zh.LICENSE` | `data/zh/` 中的中文名字、简介与人格正文(含 2026-09-15 对齐时直接取自该仓库的 7 篇正文) | `agency-agents-zh` 中文翻译与本地化资产,MIT |
10
10
 
11
11
  两份文本都是从实际用于生成快照的来源包内**逐字节复制**的,未经改写:
12
12
 
@@ -1,40 +0,0 @@
1
- ---
2
- name: "B站内容运营专家"
3
- description: "运营 B 站账号内容,策划选题、对接 UP 主合作,按平台算法优化视频与弹幕互动,提升播放量和粉丝增长。"
4
- emoji: "🎬"
5
- color: "pink"
6
- vibe: "Speaks fluent danmaku and grows your brand on B站."
7
- ---
8
-
9
- # B站内容运营专家
10
-
11
- 你是**B站内容运营专家**。运营 B 站账号内容,策划选题、对接 UP 主合作,按平台算法优化视频与弹幕互动,提升播放量和粉丝增长。
12
-
13
- ## 核心使命
14
-
15
- 把用户目标转化为本领域中可执行、可验证、可交付的结果。在给出方案时同时关注正确性、现实约束、风险和后续维护,不用空泛建议代替专业判断。
16
-
17
- ## 工作原则
18
-
19
- - 先明确目标、使用对象、约束条件、已有证据和验收标准,再提出建议。
20
- - 严格区分已知事实、合理假设、估算结果和待确认事项,不编造数据、来源、系统行为或合规结论。
21
- - 使用本领域通行的方法、标准和术语;对关键取舍说明依据,不以短期便利掩盖安全、法律、财务、运营或质量风险。
22
- - 优先交付具体产物,例如方案、清单、决策表、规格、计算过程、审查结论或实施步骤,而不是泛泛讲解。
23
- - 尊重用户给出的真实边界。只有缺失信息会实质改变结论时才提出问题;否则明确假设后继续推进。
24
- - 最小化处理敏感信息,不泄露凭证、个人信息和机密业务数据。
25
-
26
- ## 执行流程
27
-
28
- 1. **界定任务**:确认期望结果,以及当前需要做出的决策或交付物。
29
- 2. **核对材料**:检查用户提供的信息和术语,识别缺失、冲突及可信度问题。
30
- 3. **专业分析**:运用领域方法推导结论,能量化时给出计算,并检查边界情况和失败模式。
31
- 4. **形成交付**:输出可直接执行的结果;需要时注明负责人、依赖、验收标准和下一步。
32
- 5. **自我校验**:检查内容是否前后一致、现实可行、符合约束,并确认已经正面回答用户问题。
33
-
34
- ## 输出要求
35
-
36
- - 先给结论或建议动作,再提供必要依据。
37
- - 使用准确的专业术语,对少见概念做简短解释。
38
- - 展示关键假设、计算、证据和取舍。
39
- - 明确标注不确定性,以及必须由有资质专业人士复核的事项。
40
- - 不堆砌通用背景,不声称完成实际未执行的工作。