dsh-plugin-t-expert 0.3.88 → 0.3.89

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.
@@ -3,14 +3,11 @@
3
3
  * Remote 线格式的**单一 schema 源**(A-2 / A-3)。
4
4
  *
5
5
  * 为什么要有这个文件:这些 schema 过去在 host(`lib/remote.js`)与客户端(`src/client/`)
6
- * 各写了一份,**13 个同名定义逐字重复**。加一个 remote 方法或改一个字段要改两处,漏改一处
6
+ * 各写了一份。加一个 remote 方法或改一个字段要改两处,漏改一处
7
7
  * 不会在加载期报错,只会在调用期炸(或更糟:静默把字段剥掉)。
8
8
  *
9
9
  * 权威在 **host**:它同时是生产方与校验方(`lib/remote.js` 的 DESCRIPTORS 用的就是这些对象),
10
10
  * 客户端只是消费方。所以统一后 host 的校验强度不变,客户端变严——方向是安全的。
11
- * 统一过程中修正了两处真实不一致:
12
- * 1. `squadInputSchema.key` 客户端原来没有 `min(1).max(64)`;
13
- * 2. 客户端原来把团队快照叫 `catalogSchema`、把 `teamMemberSchema` 内联写了一遍。
14
11
  *
15
12
  * ⚠️ 本文件只放**纯 schema**,不放 host 专属的 `descriptor()` 或客户端专属的 `direct()` 信封
16
13
  * —— 两端信封的字段(`id` 前缀、`result` 形状)本来就不同,强行合并反而更脆。
@@ -80,7 +77,6 @@ export const enabledStateSchema = z.object({
80
77
  * 加新键要同时改 host(lib/index.js 的 settingsSchema 与 getUiPrefs)与客户端消费处。
81
78
  */
82
79
  export const uiPrefsSchema = z.object({
83
- showTaskBadge: z.boolean(),
84
80
  showScheduleButton: z.boolean(),
85
81
  // 窗口右上角「活跃指示」是否显示。宿主**总是**返回布尔(缺省按 true 算),所以这里必填。
86
82
  showActivePulse: z.boolean(),
@@ -88,111 +84,12 @@ export const uiPrefsSchema = z.object({
88
84
 
89
85
  /** `setUiPrefs` 的入参:想改哪个传哪个(未传的键保持原值)。 */
90
86
  export const uiPrefsPatchSchema = z.object({
91
- showTaskBadge: z.boolean().optional(),
92
87
  showScheduleButton: z.boolean().optional(),
93
88
  showActivePulse: z.boolean().optional(),
94
89
  });
95
90
 
96
91
  export const promptSchema = z.object({ prompt: z.string() });
97
92
 
98
- export const squadMemberSchema = z.object({
99
- slug: z.string(),
100
- role: z.string(),
101
- name: z.string(),
102
- nameZh: z.string(),
103
- division: z.string(),
104
- divisionZh: z.string(),
105
- known: z.boolean(),
106
- });
107
-
108
- export const squadSchema = z.object({
109
- key: z.string(),
110
- description: z.string(),
111
- aliases: z.array(z.string()),
112
- taskPlanning: z.enum(["captain", "seed"]),
113
- enabled: z.boolean(),
114
- members: z.array(squadMemberSchema),
115
- });
116
-
117
- export const rosterExpertSchema = z.object({
118
- slug: z.string(),
119
- name: z.string(),
120
- nameZh: z.string(),
121
- division: z.string(),
122
- divisionZh: z.string(),
123
- enabled: z.boolean(),
124
- conflict: z.boolean(),
125
- });
126
-
127
- export const squadsSnapshotSchema = z.object({
128
- revision: z.string(),
129
- squads: z.array(squadSchema),
130
- experts: z.array(rosterExpertSchema),
131
- lastBuild: z.object({ at: z.number(), ok: z.boolean(), output: z.string() }),
132
- });
133
-
134
- export const teamMemberSchema = z.object({
135
- name: z.string(),
136
- role: z.string(),
137
- status: z.string(),
138
- activity: z.string(),
139
- done: z.number(),
140
- total: z.number(),
141
- });
142
-
143
- export const teamSchema = z.object({
144
- teamId: z.string(),
145
- name: z.string(),
146
- workspace: z.string(),
147
- // 2026-09-15:teamId 是团队名 slug 化的结果,**跨工作区会重名**,客户端把 workspacePath
148
- // 回传给 stop/deleteTeam 才能精确定位(strict schema 不声明就会被剥掉)。
149
- // 两个字段都 `.optional()` 是**兼容闸门**:客户端产物热重载,host 可能是旧的(不返回它们)——
150
- // 那时必需字段会让 `teamsSnapshotSchema.parse` 直接失败、列表**整片空白**。可选之后退化为
151
- // 「按 halted 判定按钮 + 按 teamId 全量遍历」的旧行为,而不是白屏。
152
- workspacePath: z.string().optional(),
153
- /** 队长会话此刻是否在运行:客户端据此决定按钮叫「停止」还是「归档」。 */
154
- captainActive: z.boolean().optional(),
155
- captainSessionId: z.string(),
156
- phase: z.string(),
157
- halted: z.boolean(),
158
- members: z.array(teamMemberSchema),
159
- tasks: z.array(z.object({ id: z.string(), subject: z.string(), status: z.string(), assignee: z.string() })),
160
- });
161
-
162
- export const teamsSnapshotSchema = z.object({ teams: z.array(teamSchema) });
163
-
164
- export const startResultSchema = z.object({
165
- profile: z.string(),
166
- label: z.string(),
167
- members: z.number(),
168
- });
169
-
170
- export const stopResultSchema = z.object({
171
- teamName: z.string(),
172
- cancelledTasks: z.number(),
173
- alreadyHalted: z.boolean(),
174
- // 2026-09-15 起,「停止」在队长会话已结束(或团队早已 halted)时改为**归档**:
175
- // 这个字段让面板知道该报「已停止」还是「已归档」。
176
- archived: z.boolean(),
177
- });
178
-
179
- /** 「删除团队」的结果:与「停止/归档」不同,这是不可逆的清理。 */
180
- export const deleteTeamResultSchema = z.object({
181
- teamName: z.string(),
182
- deleted: z.boolean(),
183
- // 删掉的是归档里那份(而不是还挂在「运行中」的那份)
184
- wasArchived: z.boolean(),
185
- });
186
-
187
- /** 面板提交的小队定义(saveSquads 的入参)。 */
188
- export const squadInputSchema = z.object({
189
- key: z.string().min(1).max(64),
190
- description: z.string(),
191
- aliases: z.array(z.string()),
192
- taskPlanning: z.enum(["captain", "seed"]),
193
- enabled: z.boolean(),
194
- members: z.array(z.string()),
195
- });
196
93
 
197
94
  // ---------------------------------------------------------------- 定时事项
198
95
  // 与 @weibaohui/dsh-tasks 的线格式**刻意不同名**:两个插件可以同时安装,各自的 schema / 服务名 /
@@ -214,6 +111,8 @@ export const scheduleItemSchema = z.object({
214
111
  cron: z.string(),
215
112
  enabled: z.boolean(),
216
113
  workspaceId: z.string().optional(),
114
+ /** 绑定时记下的工作区目录:工作区被删后凭它「重建」回到原路径。面板也会在编辑表单里显示它。 */
115
+ workspacePath: z.string().optional(),
217
116
  createdAt: z.string(),
218
117
  updatedAt: z.string(),
219
118
  lastRunAt: z.string().optional(),
@@ -243,6 +142,8 @@ export const scheduleInputSchema = z.object({
243
142
  enabled: z.boolean(),
244
143
  /** 空串 = 不绑定工作区(显式解绑也走这个值)。 */
245
144
  workspaceId: z.string().max(200).optional(),
145
+ /** 绑定时记下的工作区目录;老数据可缺省,按当前选中的 workspace 自动补。 */
146
+ workspacePath: z.string().max(1024).optional(),
246
147
  });
247
148
 
248
149
  /** 编辑时的局部更新(未传的键保持原值)。 */
package/lib/remote.js CHANGED
@@ -7,10 +7,10 @@
7
7
  */
8
8
  import { Remote, RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
9
9
  import { z } from "zod";
10
- import { CATALOG_SERVICE, SCHEDULE_SERVICE, SETTINGS_NAMESPACE, SQUAD_SERVICE, TEAM_SERVICE, UNKNOWN_SLUG_ERROR_CODE } from "./index.js";
10
+ import { CATALOG_SERVICE, SCHEDULE_SERVICE, SETTINGS_NAMESPACE, UNKNOWN_SLUG_ERROR_CODE } from "./index.js";
11
11
 
12
12
  /**
13
- * 领域码形态:`<domain>/<reason>`(如 `tTeam/squads-invalid`)。
13
+ * 领域码形态:`<domain>/<reason>`(如 `tTeam/schedule-invalid`)。
14
14
  *
15
15
  * 只有通过它校验的 `error.code` 才允许当 wire code 透传 —— 系统 errno(`EACCES`…)不是领域码,
16
16
  * 透传出去会让客户端按 code 分支时把「文件权限」误认成业务失败类型(D-17)。口径与宿主网关一致
@@ -28,22 +28,11 @@ import {
28
28
  enabledStateSchema,
29
29
  expertSchema,
30
30
  promptSchema,
31
- rosterExpertSchema,
32
31
  scheduleInputSchema,
33
32
  schedulePatchSchema,
34
33
  schedulePreviewSchema,
35
34
  scheduleWorkspaceSchema,
36
35
  scheduleSnapshotSchema,
37
- squadInputSchema,
38
- squadMemberSchema,
39
- squadSchema,
40
- squadsSnapshotSchema,
41
- startResultSchema,
42
- stopResultSchema,
43
- deleteTeamResultSchema,
44
- teamMemberSchema,
45
- teamSchema,
46
- teamsSnapshotSchema,
47
36
  uiPrefsPatchSchema,
48
37
  uiPrefsSchema,
49
38
  } from "./remote-schemas.js";
@@ -65,7 +54,7 @@ function descriptor(method, parameters, typeSymbol, schema) {
65
54
  const DESCRIPTORS = [
66
55
  descriptor("getCatalog", [], "TTeamCatalog", catalogSnapshotSchema),
67
56
  descriptor("getUiPrefs", [], "TTeamUiPrefs", uiPrefsSchema),
68
- // 入参从「单个 boolean」改成 patch 对象:界面偏好不再只有任务徽章一项,
57
+ // 入参从「单个 boolean」改成 patch 对象:界面偏好不止一项,
69
58
  // 加键不必再加方法(未传的键保持原值,host 侧做兜底)。
70
59
  descriptor("setUiPrefs", [
71
60
  {
@@ -75,7 +64,7 @@ const DESCRIPTORS = [
75
64
  codec: { mode: "strict", typeSymbol: "TTeamUiPrefsPatch", schema: uiPrefsPatchSchema, create: () => uiPrefsPatchSchema },
76
65
  },
77
66
  ], "TTeamUiPrefs", uiPrefsSchema),
78
- // 定时事项:全部写操作都回传刷新后的整份快照,面板拿到就能直接重绘(与名册/小队同一口径)。
67
+ // 定时事项:全部写操作都回传刷新后的整份快照,面板拿到就能直接重绘(与名册同一口径)。
79
68
  descriptor("getSchedule", [], "TTeamSchedule", scheduleSnapshotSchema),
80
69
  descriptor("createScheduleItem", [
81
70
  {
@@ -159,80 +148,6 @@ const DESCRIPTORS = [
159
148
  codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(64), create: () => z.string().min(1).max(64) },
160
149
  },
161
150
  ], "TTeamPrompt", promptSchema),
162
- descriptor("getSquads", [], "TTeamSquads", squadsSnapshotSchema),
163
- descriptor("saveSquads", [
164
- {
165
- name: "squads",
166
- wire: "squads",
167
- source: "json",
168
- codec: { mode: "strict", typeSymbol: "TTeamSquadInput[]", schema: z.array(squadInputSchema).min(1).max(48), create: () => z.array(squadInputSchema).min(1).max(48) },
169
- },
170
- {
171
- name: "expectedRevision",
172
- wire: "expectedRevision",
173
- source: "json",
174
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(64), create: () => z.string().max(64) },
175
- },
176
- ], "TTeamSquads", squadsSnapshotSchema),
177
- descriptor("startSquad", [
178
- {
179
- name: "profileKey",
180
- wire: "profileKey",
181
- source: "json",
182
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(64), create: () => z.string().min(1).max(64) },
183
- },
184
- {
185
- name: "sessionId",
186
- wire: "sessionId",
187
- source: "json",
188
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(128), create: () => z.string().max(128) },
189
- },
190
- ], "TTeamStartResult", startResultSchema),
191
- descriptor("getTeams", [
192
- {
193
- name: "archived",
194
- wire: "archived",
195
- source: "json",
196
- codec: { mode: "strict", typeSymbol: "boolean", schema: z.boolean(), create: () => z.boolean() },
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
- // 标上之后旧客户端照旧能调,宿主退回「全量遍历」的旧行为。
205
- descriptor("stopTeam", [
206
- {
207
- name: "teamId",
208
- wire: "teamId",
209
- source: "json",
210
- codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128), create: () => z.string().min(1).max(128) },
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), create: () => z.string().max(1024) },
218
- },
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), create: () => 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), create: () => z.string().max(1024) },
234
- },
235
- ], "TTeamDeleteResult", deleteTeamResultSchema),
236
151
  // 自建专家的增删改:都回传刷新后的整份名册快照,面板拿到就能直接重绘。
237
152
  descriptor("createExpert", [
238
153
  {
@@ -358,7 +273,7 @@ class TTeamRemote extends TypertRemoteService {
358
273
  /**
359
274
  * host 侧服务缺失的统一领域错误。
360
275
  *
361
- * 三个访问器(catalog / squads / teams)共用它:**不要**裸抛 `Error` —— 裸 Error 过线会被
276
+ * 一个访问器(catalog)共用它:**不要**裸抛 `Error` —— 裸 Error 过线会被
362
277
  * 网关折成 `gateway/internal`,领域信息全丢;也**不要**把「host 没加载」塞进别的领域码
363
278
  * (例如 `tTeam/missing-expert`),那会把「服务不可用」显示成「专家不存在」(D-4)。
364
279
  * @param name - 服务名(写进 details,便于面板区分是哪一个)。
@@ -410,7 +325,7 @@ class TTeamRemote extends TypertRemoteService {
410
325
  /**
411
326
  * 定时事项服务(T专家 自己的引擎,与 @weibaohui/dsh-tasks 完全独立)。
412
327
  *
413
- * 与 `teams()` 同理:缺服务时给的是**功能语境的**错误码(`tTeam/schedule-unavailable`
328
+ * 与 `catalog()` 同理:缺服务时给的是**功能语境的**错误码(`tTeam/schedule-unavailable`
414
329
  * 读起来就是「定时事项用不了」),而不是笼统的 host-unavailable。
415
330
  */
416
331
  schedule() {
@@ -527,88 +442,6 @@ class TTeamRemote extends TypertRemoteService {
527
442
  }
528
443
  }
529
444
 
530
- /** 小队定义服务(设置页「小队」标签)。 */
531
- squads() {
532
- const service = this.ctx.get(SQUAD_SERVICE);
533
- if (service === undefined) {
534
- throw this.hostUnavailable(SQUAD_SERVICE, "T专家 小队服务不可用(host 插件未加载)");
535
- }
536
- return service;
537
- }
538
-
539
- /**
540
- * 团队运行服务(设置页「团队」标签)。
541
- *
542
- * 这一处**刻意偏离**「缺服务一律 tTeam/host-unavailable」的统一口径,原因有两条,都记录在此:
543
- * ① 语义上仍准确 —— 对 `startSquad` 来说「host 没挂团队服务」就是「这支小队起不来」,
544
- * `tTeam/start-failed` 是可读的,而 `host-unavailable` 会丢掉「用户点的是启动」这层上下文;
545
- * ② 向后兼容 —— 既有自检(verify.mjs 的 "startSquad 未知小队 → tTeam/start-failed")与客户端都把
546
- * 这个码当作「启动失败」的分类键,改成新码会让唯一被断言的分类断裂。
547
- * 缺的是 `catalog`/`squads` 那两个访问器(没有等价的历史承诺),它们统一走 hostUnavailable()。
548
- */
549
- teams() {
550
- const service = this.ctx.get(TEAM_SERVICE);
551
- if (service === undefined) {
552
- // @ts-expect-error 上游类型缺口:同上,tTeam/* 业务码不在闭合的 RemoteErrorDetailsMap 里。
553
- throw new RemoteError("tTeam/start-failed", "T专家 团队服务不可用(host 插件未加载)", { service: TEAM_SERVICE });
554
- }
555
- return service;
556
- }
557
-
558
- /** 小队定义 + 名册(成员选择器要的数据)。 */
559
- async getSquads() {
560
- try {
561
- return this.squads().list();
562
- } catch (error) {
563
- throw this.businessError(error, "tTeam/squads-unreadable");
564
- }
565
- }
566
-
567
- /** 写回小队定义并重编译引擎配置(失败自动回滚)。 */
568
- async saveSquads(squads, expectedRevision) {
569
- try {
570
- return this.squads().save(squads, expectedRevision);
571
- } catch (error) {
572
- throw this.businessError(error, "tTeam/squads-invalid");
573
- }
574
- }
575
-
576
- /** 从面板/弹窗里直接拉起一支小队(等价于用户敲 `/t <小队>`)。 */
577
- async startSquad(profileKey, sessionId) {
578
- try {
579
- return await this.teams().start(profileKey, sessionId);
580
- } catch (error) {
581
- throw this.businessError(error, "tTeam/start-failed");
582
- }
583
- }
584
-
585
- /** 活动团队 / 归档团队快照。 */
586
- async getTeams(archived) {
587
- try {
588
- const teams = await this.teams().list(archived === true);
589
- return { teams };
590
- } catch (error) {
591
- throw this.businessError(error, "tTeam/teams-unavailable");
592
- }
593
- }
594
-
595
- /** 停止一个团队(等价于活动面板里的停止)。`workspacePath` 用于同名团队的精确定位。 */
596
- async stopTeam(teamId, workspacePath = "") {
597
- try {
598
- return await this.teams().stop(teamId, workspacePath);
599
- } catch (error) {
600
- throw this.businessError(error, "tTeam/stop-failed");
601
- }
602
- }
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
445
 
613
446
  /**
614
447
  * 业务错误统一转 RemoteError(裸 Error 过线会退化成 gateway/internal 并丢掉 code)。
@@ -697,7 +530,7 @@ class TTeamRemote extends TypertRemoteService {
697
530
  }
698
531
 
699
532
  exposeRemoteMethods(TTeamRemote, [
700
- "getCatalog", "setEnabled", "getPrompt", "getSquads", "saveSquads", "getTeams", "stopTeam", "deleteTeam", "startSquad",
533
+ "getCatalog", "setEnabled", "getPrompt",
701
534
  "getUiPrefs", "setUiPrefs",
702
535
  "getSchedule", "createScheduleItem", "updateScheduleItem", "deleteScheduleItem", "runScheduleItem", "previewSchedule", "ensureScheduleWorkspace",
703
536
  "createExpert", "updateExpert", "deleteExpert",
package/lib/schedule.js CHANGED
@@ -9,7 +9,7 @@
9
9
  * 提交给会话的提示词用 `source.kind: "user"`(聊天右侧才渲染用户气泡;`plugin` 源会被收成 inject)。
10
10
  *
11
11
  * 三件事在这里闭环:
12
- * 1. 持久化:一个 JSON 文件,原子写(与 `teams.json` 同级、同风格);
12
+ * 1. 持久化:一个 JSON 文件,原子写(与名册数据同风格);
13
13
  * 2. 调度:每个启用项一个 croner job,宿主启动时全量重建,卸载时全部 stop;
14
14
  * 3. 执行:新建 agent 会话 → 挂部署默认 preset(不挂就没有工具)→ 提交提示词。
15
15
  *
@@ -23,7 +23,7 @@ import { writeFileAtomic } from "./catalog.js";
23
23
 
24
24
  /** 定时事项服务名(remote 侧的服务访问器用它)。 */
25
25
  export const SCHEDULE_SERVICE = "tTeamSchedule";
26
- /** 数据文件名(落在 T专家 数据根下,与 teams.json 同级)。 */
26
+ /** 数据文件名(落在 T专家 数据根下)。 */
27
27
  export const SCHEDULE_FILE_NAME = "schedule.json";
28
28
  /** 每条事项保留的执行记录条数(最旧的先丢)。 */
29
29
  export const MAX_RUNS = 20;
@@ -96,7 +96,12 @@ export function normalizeInput(input, t) {
96
96
  const cron = validateCron(raw.cron, t);
97
97
  const enabled = raw.enabled !== false;
98
98
  const workspaceId = typeof raw.workspaceId === "string" && raw.workspaceId !== "" ? raw.workspaceId : undefined;
99
- return { title, prompt, cron, enabled, ...(workspaceId === undefined ? {} : { workspaceId }) };
99
+ const workspacePath = typeof raw.workspacePath === "string" && raw.workspacePath !== "" ? raw.workspacePath : undefined;
100
+ return {
101
+ title, prompt, cron, enabled,
102
+ ...(workspaceId === undefined ? {} : { workspaceId }),
103
+ ...(workspacePath === undefined ? {} : { workspacePath }),
104
+ };
100
105
  }
101
106
 
102
107
  /**
@@ -109,8 +114,6 @@ export function normalizeInput(input, t) {
109
114
  * @param {(key: string, params?: Record<string, unknown>) => string} options.t - host 文案函数。
110
115
  * @param {string} options.defaultCwd - 不绑定工作区时的会话工作目录。
111
116
  * @param {boolean} [options.armed] - 初始是否启用调度(= 界面开关的当前值)。
112
- * @param {(text: string) => Promise<string>} [options.expandPrompt] - 提交前的提示词改写
113
- * (提示词里挑中的 `@专家名` 由它落成召唤指令)。缺省就是原文。
114
117
  * @param {(path: string, title?: string) => Promise<{ id: string, title: string, path: string }>} [options.ensureWorkspace]
115
118
  * - 准备定时任务的工作区(默认「定时任务」工作区,或面板里填的自定义目录)。缺省则面板该项不可用。
116
119
  */
@@ -118,10 +121,6 @@ export function createScheduleEngine(options) {
118
121
  const { ctx, file, logger, t, defaultCwd } = options;
119
122
  /** 工作区准备(默认「定时任务」工作区 / 自定义目录),由 host 注入。 */
120
123
  const ensureWorkspace = typeof options.ensureWorkspace === "function" ? options.ensureWorkspace : undefined;
121
- /** 提交前的提示词改写:没有注入就原样提交(定时任务本身不依赖它)。 */
122
- const expandPrompt = typeof options.expandPrompt === "function"
123
- ? options.expandPrompt
124
- : async (text) => text;
125
124
  /** @type {Map<string, any>} id → 事项 */
126
125
  const items = new Map();
127
126
  /** @type {Map<string, any>} id → croner job */
@@ -157,6 +156,8 @@ export function createScheduleEngine(options) {
157
156
  cron: entry.cron,
158
157
  enabled: entry.enabled !== false,
159
158
  ...(typeof entry.workspaceId === "string" && entry.workspaceId !== "" ? { workspaceId: entry.workspaceId } : {}),
159
+ // 绑定时同步记下当时的工作区目录:工作区被删后用它「重建」回到原路径,避免要用户重填。
160
+ ...(typeof entry.workspacePath === "string" && entry.workspacePath !== "" ? { workspacePath: entry.workspacePath } : {}),
160
161
  createdAt: typeof entry.createdAt === "string" ? entry.createdAt : new Date().toISOString(),
161
162
  updatedAt: typeof entry.updatedAt === "string" ? entry.updatedAt : new Date().toISOString(),
162
163
  ...(typeof entry.lastRunAt === "string" ? { lastRunAt: entry.lastRunAt } : {}),
@@ -239,11 +240,21 @@ export function createScheduleEngine(options) {
239
240
  if (agents === undefined || typeof agents.create !== "function") {
240
241
  throw new Error(t("schedule.agentsMissing"));
241
242
  }
242
- const workspace = item.workspaceId === undefined
243
+ // 工作区解析:绑过 → 查 registry;查不到 → 按 task 记的路径自动 mkdir + create。
244
+ // 这是用户口径"工作区目录没了就自动新建"的实现点。recover 已经在内部把 workspaceId 写回 items。
245
+ let workspace = item.workspaceId === undefined
243
246
  ? undefined
244
247
  : ctx.get("workspaceRegistry")?.get?.(item.workspaceId);
245
248
  if (item.workspaceId !== undefined && workspace === undefined) {
246
- throw new Error(t("schedule.workspaceMissing", { id: item.workspaceId }));
249
+ const recovered = await tryAutoRecoverWorkspace(item);
250
+ if (recovered === null || recovered === undefined) {
251
+ // 实在没路径可恢复(老数据迁移后没记路径):给个明确报错让面板呈现。
252
+ const hint = typeof item.workspacePath === "string" && item.workspacePath !== ""
253
+ ? t("schedule.workspaceMissingWithPath", { id: item.workspaceId, path: item.workspacePath })
254
+ : t("schedule.workspaceMissing", { id: item.workspaceId });
255
+ throw new Error(hint);
256
+ }
257
+ workspace = recovered;
247
258
  }
248
259
  const selection = ctx.get("agentDefaultModel")?.currentSelection?.() ?? {};
249
260
  const sessionId = `session-${randomUUID()}`;
@@ -276,14 +287,10 @@ export function createScheduleEngine(options) {
276
287
  } catch {
277
288
  /* 标题没钉上也照样执行 */
278
289
  }
279
- // 提示词里可能带 `@专家名`(面板里挑中的那位):交给注入的改写器落成召唤指令。
280
- // 改写失败不致命 —— 退回原文提交,任务照跑,只在日志里留一句可查的线索。
281
- let promptText = item.prompt;
282
- try {
283
- promptText = await expandPrompt(item.prompt);
284
- } catch (error) {
285
- logger?.warn?.(`[t-team] 定时任务「${item.title}」的提示词改写失败,按原文提交:${error instanceof Error ? error.message : String(error)}`);
286
- }
290
+ // 提示词**原样提交**:定时任务开的新会话与手动会话挂同一份 system prompt,里面已经写着
291
+ // 「看到 `@专家名` 就 summon_t_expert FIRST」。这里再做一次改写只会让同一个任务在定时任务
292
+ // 里显示成一长段模板、在手动对话里却只是 `@名字`,两条入口观感不一致(用户实测反馈)。
293
+ const promptText = item.prompt;
287
294
  // 必须用 source.kind: "user":DSH 聊天把非 user 的 user/message 收成左侧
288
295
  // inject/context(plugin 源只显示插件名,正文不进右侧气泡)。定时任务的指令
289
296
  // 本身就是用户写的提示词,应当像手打那条一样出现在对话右侧。
@@ -362,12 +369,15 @@ export function createScheduleEngine(options) {
362
369
  cron: source.cron === undefined ? current.cron : source.cron,
363
370
  enabled: source.enabled === undefined ? current.enabled : source.enabled,
364
371
  workspaceId: source.workspaceId === undefined ? current.workspaceId : source.workspaceId,
372
+ // workspacePath 与 workspaceId 一同从表单过来;老数据没有也不补(创建时记的是绑定那一刻的目录)。
373
+ workspacePath: source.workspacePath === undefined ? current.workspacePath : source.workspacePath,
365
374
  };
366
375
  const fields = normalizeInput(merged, t);
367
376
  const next = { ...current, ...fields, updatedAt: new Date().toISOString() };
368
377
  // 展开旧记录会把旧的 workspaceId 一起带过来:用户把绑定改回「不绑定」时
369
378
  // fields 里没有这个键,必须显式删掉,否则界面上看着解绑了、执行时还挂在旧工作区。
370
379
  if (fields.workspaceId === undefined) delete next.workspaceId;
380
+ if (fields.workspacePath === undefined) delete next.workspacePath;
371
381
  items.set(id, next);
372
382
  scheduleOne(next);
373
383
  await persist();
@@ -407,6 +417,59 @@ export function createScheduleEngine(options) {
407
417
  return { armed };
408
418
  }
409
419
 
420
+ /**
421
+ * 自动恢复:执行路径发现工作区不见了,但任务里记着目录 → 顺着把工作区建回来。
422
+ * 这是用户口径"工作区目录没了就自动新建"的实现点:
423
+ * - execute 拿到 registry.get 拿不到的 workspace 时,先看 item.workspacePath:
424
+ * · 路径已记录:按这个路径 ensureWorkspace(**会顺带 mkdir 目录**,宿主 createCanonical 内部对同路径幂等复用)。
425
+ * · 没路径:只能抛错让用户去左侧手动建一次工作区(之后 schedule.json 里的 workspaceId 会被自动建回。
426
+ * 老数据迁移后第一次失败就属于这种情况)。
427
+ * - 恢复成功后顺便把 workspaceId 刷回去(**mutate 传入的 item + items.set 同步**:
428
+ * execute 自身就是单飞序列,传入的 item 是 Map 里的那条引用;mutate 后 withRun 能
429
+ * 拿到最新字段,items.set + persist 保证落盘,调度下次直接命中 registry)。
430
+ *
431
+ * @returns 恢复出来的 workspace(已经是 registry 里的实体);返回 null 表示不可恢复。
432
+ */
433
+ async function tryAutoRecoverWorkspace(item) {
434
+ const finalPath = resolveRebuildPath(item, undefined);
435
+ if (finalPath === "") return null;
436
+ try {
437
+ const workspace = await ensureOrFail(finalPath);
438
+ // 1) mutate 传入的 item:execute 后面的 withRun(item, ...) 直接看到新 workspaceId/Path。
439
+ item.workspaceId = workspace.id;
440
+ item.workspacePath = workspace.path;
441
+ item.updatedAt = new Date().toISOString();
442
+ delete item.lastRunError;
443
+ // 2) items.set 触发 Map 引用更新(与 mutate 实际指向同一对象,但保持语义一致);
444
+ // persist 把这条新字段落到磁盘。
445
+ items.set(item.id, item);
446
+ await persist();
447
+ logger?.info?.(`[t-team] 定时事项「${item.title}」绑定的工作区不存在,已按原路径 ${finalPath} 自动重建(id=${workspace.id})。`);
448
+ return workspace;
449
+ } catch (error) {
450
+ logger?.warn?.(`[t-team] 定时事项「${item.title}」自动恢复工作区失败:${error instanceof Error ? error.message : String(error)}`);
451
+ return null;
452
+ }
453
+ }
454
+
455
+ /** 抽取"用哪条 path"的判定:外部传入优先,否则用 task 记的。两边都没有就返回空串。 */
456
+ function resolveRebuildPath(item, override) {
457
+ if (typeof override === "string" && override.trim() !== "") return override.trim();
458
+ if (typeof item?.workspacePath === "string" && item.workspacePath.trim() !== "") return item.workspacePath.trim();
459
+ return "";
460
+ }
461
+
462
+ /**
463
+ * 调宿主的工作区准备能力拿实体。宿主若未注入(理论上不会发生,scheduleEngine 必然随插件注入)
464
+ * 就在这里抛错,调用方自己决定是抛还是降级。
465
+ */
466
+ function ensureOrFail(path) {
467
+ if (ensureWorkspace === undefined) {
468
+ return Promise.reject(new Error("宿主没有提供工作区准备能力(ensureWorkspace)。"));
469
+ }
470
+ return ensureWorkspace(path, undefined);
471
+ }
472
+
410
473
  /** 卸载:停掉全部定时器(数据留着,下次启动重建)。 */
411
474
  function dispose() {
412
475
  for (const id of [...jobs.keys()]) stopJob(id);
@@ -421,12 +484,19 @@ export function createScheduleEngine(options) {
421
484
  remove,
422
485
  runNow,
423
486
  preview: (cron) => ({ fires: previewRuns(cron, t) }),
424
- /** 给面板用:确保定时任务的默认工作区(或自定义目录)存在,返回 { id, title, path }。 */
425
- ensureWorkspace: (path, title) => {
487
+ /**
488
+ * 给面板用:确保定时任务的默认工作区(或自定义目录)存在,返回精简 { id, title, path }。
489
+ *
490
+ * 注意:这里不直接转发 host 注入的 ensureWorkspace,因为后者现在透的是完整 entity
491
+ * (含 attachSession 等方法引用),不能塞进 remote 的 zod 严格 schema;面板用不上那些方法,
492
+ * 拿 id/title/path 就够了。
493
+ */
494
+ ensureWorkspace: async (path, title) => {
426
495
  if (ensureWorkspace === undefined) {
427
- return Promise.reject(new Error("宿主没有提供工作区准备能力(ensureWorkspace)。"));
496
+ throw new Error("宿主没有提供工作区准备能力(ensureWorkspace)。");
428
497
  }
429
- return ensureWorkspace(path, title);
498
+ const entity = await ensureWorkspace(path, title);
499
+ return { id: String(entity.id), title: String(entity.title ?? ""), path: String(entity.path ?? "") };
430
500
  },
431
501
  setArmed,
432
502
  isArmed: () => armed,
package/lib/skill.js CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * 随包发布的 skill:把「T专家 运维」做成**随 npm 包分发**的 skill。
4
4
  *
5
- * 为什么是 skill 而不是再加一段 systemPrompt:这些内容只在特定任务里需要(改名册/小队/装机/发布),
5
+ * 为什么是 skill 而不是再加一段 systemPrompt:这些内容只在特定任务里需要(改名册/装机/发布),
6
6
  * 常驻提示段是纯浪费 token;skill 的 description 本来就是路由面。
7
7
  *
8
8
  * 形态:`<包根>/skills/<name>/SKILL.md` 是标准 skill 文件(任何 skill 扫描器都能直接发现),
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "dsh-plugin-t-expert",
3
- "version": "0.3.88",
3
+ "version": "0.3.89",
4
4
  "type": "module",
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).",
5
+ "description": "T Expert — a 323-expert, 22-division roster with full Chinese translations, as a standalone DeepSeek Harness plugin.",
6
6
  "license": "MIT",
7
7
  "author": "jiuaiwo",
8
8
  "keywords": [
@@ -10,7 +10,6 @@
10
10
  "dsh-plugin",
11
11
  "deepseek-harness",
12
12
  "t-expert",
13
- "t-team",
14
13
  "expert-roster",
15
14
  "experts",
16
15
  "subagent",
@@ -44,15 +43,10 @@
44
43
  "data/experts",
45
44
  "data/zh",
46
45
  "data/source.json",
47
- "data/team-profiles.py",
48
- "data/teams.json",
49
- "data/teams.resolved.json",
50
- "data/t-team.config.json",
51
46
  "cordis.patch.yml",
52
47
  "README.md",
53
48
  "LICENSE",
54
49
  "THIRD-PARTY-NOTICES",
55
- "vendor/dsh-agent-teams/LICENSE",
56
50
  "vendor/third-party-licenses"
57
51
  ],
58
52
  "dsh": {
@@ -79,8 +73,7 @@
79
73
  "render-smoke": "node tools/render-smoke.mjs",
80
74
  "verify": "node tools/verify.mjs",
81
75
  "sync-data": "node tools/sync-data.mjs",
82
- "prepublishOnly": "npm run typecheck && npm run check-client && npm run render-smoke && npm run build && npm run verify && npm run invariants && node tools/sync-data.mjs --check",
83
- "invariants": "node tools/invariants.mjs"
76
+ "prepublishOnly": "npm run typecheck && npm run check-client && npm run render-smoke && npm run build && npm run verify && node tools/sync-data.mjs --check"
84
77
  },
85
78
  "peerDependencies": {
86
79
  "@deepseek-ai/cordis": "^4.0.1",
@@ -19,4 +19,3 @@
19
19
  但**本插件不打包它的任何代码**,只用到了上面这两份 MIT 授权的资产,
20
20
  因此这里只需分发这两份 MIT 文本。
21
21
 
22
- 内置团队引擎的许可证在 `../dsh-agent-teams/LICENSE`(同样随包分发)。