dsh-plugin-t-expert 0.2.16 → 0.3.24

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/lib/i18n.js CHANGED
@@ -48,6 +48,21 @@ const ZH = {
48
48
  "summon.batchHeader": "批量召唤 {total} 位专家:成功 {ok} / 失败 {failed}",
49
49
  "summon.itemOk": "## {expert}\n{answer}",
50
50
  "summon.itemFail": "## {expert}\n失败:{error}",
51
+ // ---- 定时任务(host 侧只负责校验与执行失败的原因文案;面板文案在客户端字典里)----
52
+ "schedule.cronEmpty": "cron 定时器不能为空",
53
+ "schedule.invalidCron": "cron 表达式无法解析:{detail}",
54
+ "schedule.inputInvalid": "定时任务的入参必须是一个对象",
55
+ "schedule.titleEmpty": "定时任务的标题不能为空",
56
+ "schedule.titleTooLong": "标题超过 {limit} 个字符",
57
+ "schedule.promptEmpty": "定时任务的提示词不能为空",
58
+ "schedule.promptTooLong": "提示词超过 {limit} 个字符",
59
+ "schedule.itemMissing": "找不到定时任务「{id}」(它可能已被删除)",
60
+ "schedule.tooManyItems": "定时任务最多 {limit} 条:请先删掉一些再新建",
61
+ "schedule.agentsMissing": "宿主没有可用的 agents 服务,无法为定时任务新建会话",
62
+ "schedule.workspaceMissing": "定时任务绑定的工作区「{id}」不存在(可能已被删除)",
63
+ "schedule.expertInvoke": "请用 summon_t_expert 召唤专家「{name}」来完成下面这件事,并原样转述它的结论;如果该工具不可用,就按这位专家的规范自己完成。\n\n任务:{task}",
64
+ "schedule.expertInvokeMany": "请用 summon_t_experts 同时召唤这些专家来完成下面这件事,并原样转述各自的结论:{names}。如果该工具不可用,就按这些专家的规范自己完成。\n\n任务:{task}",
65
+ "schedule.disabled": "定时任务功能当前已关闭:请在 设置 → T专家 里重新打开「显示定时任务按钮」",
51
66
  };
52
67
 
53
68
  const EN = {
@@ -95,6 +110,21 @@ const EN = {
95
110
  "summon.batchHeader": "Summoned {total} experts: {ok} succeeded / {failed} failed",
96
111
  "summon.itemOk": "## {expert}\n{answer}",
97
112
  "summon.itemFail": "## {expert}\nfailed: {error}",
113
+ // ---- Scheduled tasks (the host owns validation/execution failure text; the panel owns its own) ----
114
+ "schedule.cronEmpty": "The cron schedule must not be empty",
115
+ "schedule.invalidCron": "Cannot parse the cron expression: {detail}",
116
+ "schedule.inputInvalid": "A scheduled task must be an object",
117
+ "schedule.titleEmpty": "A scheduled task needs a title",
118
+ "schedule.titleTooLong": "The title exceeds {limit} characters",
119
+ "schedule.promptEmpty": "A scheduled task needs a prompt",
120
+ "schedule.promptTooLong": "The prompt exceeds {limit} characters",
121
+ "schedule.itemMissing": "Scheduled task \"{id}\" not found (it may have been deleted)",
122
+ "schedule.tooManyItems": "At most {limit} scheduled tasks: delete some before adding more",
123
+ "schedule.agentsMissing": "The host has no usable agents service, so no session can be created for this scheduled task",
124
+ "schedule.workspaceMissing": "The workspace bound to this scheduled task (\"{id}\") no longer exists",
125
+ "schedule.expertInvoke": "Summon the expert \"{name}\" with summon_t_expert to carry out the task below, and relay its findings verbatim; if that tool is unavailable, do it yourself following that expert's playbook.\n\nTask: {task}",
126
+ "schedule.expertInvokeMany": "Summon these experts together with summon_t_experts to carry out the task below, and relay each of their findings verbatim: {names}. If that tool is unavailable, do it yourself following those experts' playbooks.\n\nTask: {task}",
127
+ "schedule.disabled": "Scheduled tasks are currently turned off: re-enable \"Show scheduled tasks button\" in Settings → T Expert",
98
128
  };
99
129
 
100
130
  /** 取文案并按 {name} 占位替换。 */
package/lib/index.js CHANGED
@@ -12,8 +12,8 @@
12
12
  */
13
13
  import { homedir } from "node:os";
14
14
  import { existsSync, readFileSync, readdirSync } from "node:fs";
15
- import { stat, rmdir } from "node:fs/promises";
16
- import { dirname, join } from "node:path";
15
+ import { mkdir, stat, rmdir } from "node:fs/promises";
16
+ import { basename, dirname, join, resolve } from "node:path";
17
17
  import schema from "@deepseek-ai/schemastery";
18
18
  import { defineTool } from "@deepseek-ai/dsh-tools";
19
19
  import {
@@ -46,6 +46,9 @@ import { registerTeamCommand } from "./command.js";
46
46
  import { createSquadService } from "./squads.js";
47
47
  import { SNAPSHOT_DIR } from "./bootstrap.js";
48
48
  import { checkPlan } from "./plan-check.js";
49
+ // 定时事项引擎:T专家 自己的实现(JSON 文件 + croner),与 @weibaohui/dsh-tasks 零共享 ——
50
+ // 两个插件允许同时安装同时可用,所以存储、服务名、错误码、消息来源全都各走各的。
51
+ import { SCHEDULE_FILE_NAME, SCHEDULE_SERVICE, createScheduleEngine } from "./schedule.js";
49
52
  import { findTeamByCaptain } from "./teams/state.js";
50
53
  import { createUserMessage } from "@deepseek-ai/dsh-llm";
51
54
  // 内置团队引擎:随插件一起发布的品牌化产物在 lib/teams/ 下(源自第三方引擎,改名后
@@ -108,6 +111,16 @@ export const Config = schema.object({
108
111
  memberModel: schema.string(),
109
112
  /** 一支团队最多几名成员。 */
110
113
  maxMembers: schema.natural().min(1).default(8),
114
+ /**
115
+ * 定时任务默认绑定的工作区(首次使用时自动建/复用)。
116
+ *
117
+ * 为什么要有它:定时任务跑出来的会话如果"不绑定工作区",在侧栏里就没有归属,
118
+ * 一堆自动会话会散在工作区列表外面;给它们一个固定去处(默认「定时任务」工作区、
119
+ * 目录 <默认目录>/tesk)最好找。目录不存在会被建出来 —— 宿主的工作区 create
120
+ * 只接受已存在的目录。
121
+ */
122
+ scheduleWorkspaceTitle: schema.string().default("定时任务"),
123
+ scheduleWorkspaceDir: schema.string().default(join(homedir(), "tesk")),
111
124
  });
112
125
 
113
126
  /** 供 remote 服务读取名册的服务名。 */
@@ -116,6 +129,13 @@ export const CATALOG_SERVICE = "tTeamCatalog";
116
129
  export const SQUAD_SERVICE = "tTeamSquads";
117
130
  /** 团队运行服务(设置页「团队」标签列活动/归档团队、停止团队)。 */
118
131
  export const TEAM_SERVICE = "tTeamTeams";
132
+ /**
133
+ * 定时事项服务(remote 侧与服务访问器共用)。
134
+ *
135
+ * 定义在 `lib/schedule.js`(引擎与常量同处一地),这里**再导出**一次:`lib/remote.js` 与本文件
136
+ * 都从同一处取常量,避免出现两个字面量各写一遍、改名时漏改一处的经典漂移。
137
+ */
138
+ export { SCHEDULE_SERVICE };
119
139
  /** 设置命名空间(= 插件名)。 */
120
140
  export const SETTINGS_NAMESPACE = NS;
121
141
 
@@ -131,6 +151,15 @@ export const UNKNOWN_SLUG_ERROR_CODE = "tTeam/unknown-slug";
131
151
 
132
152
  const settingsSchema = schema.object({
133
153
  enabled: schema.array(schema.string()).default([]),
154
+ /** 是否自动显示右上角的任务徽章(界面偏好,关掉后只在输入区浮层里手动查看)。 */
155
+ showTaskBadge: schema.boolean().default(true),
156
+ /**
157
+ * 是否显示侧栏「定时事项」按钮(界面偏好)。
158
+ *
159
+ * 语义(用户口径):**关掉 = 定时事项功能整体停用** —— 不只是藏按钮,还要停掉全部定时触发
160
+ * (见 apply 里的 applyScheduleArmed)。任务与执行记录完整保留,重新打开即恢复调度。
161
+ */
162
+ showScheduleButton: schema.boolean().default(true),
134
163
  });
135
164
 
136
165
  /**
@@ -215,6 +244,119 @@ export function apply(ctx, config) {
215
244
  ctx.logger?.error?.(`[t-team] 名册快照同步失败,将按已有数据继续:${seeded.error}`);
216
245
  }
217
246
 
247
+ // ---- 定时事项引擎(T专家 自己的数据文件 + croner 调度)----
248
+ // 数据落在**本插件自己的数据根**里(`~/.t-team/schedule.json`,与 teams.json 同级):
249
+ // 刻意不碰宿主的 dsh_tasks 存储域,也不碰 dsh-tasks 插件的任何文件——两个插件同时装着时,
250
+ // 各自的任务列表、执行记录、定时器完全隔离,谁先加载都不影响对方。
251
+ const scheduleEngine = createScheduleEngine({
252
+ ctx,
253
+ file: join(dirname(config.root), SCHEDULE_FILE_NAME),
254
+ logger: ctx.logger,
255
+ // 文案按**当前**语言现取:错误消息会出现在面板上,切了语言不该还留着旧语言。
256
+ t: (key, params) => t(locale(), key, params),
257
+ defaultCwd: process.cwd(),
258
+ // 提交前的提示词改写:提示词里挑中的 `@专家名` 由它在执行时落成召唤指令。
259
+ expandPrompt: (text) => expandSchedulePrompt(text),
260
+ // 工作区需求:定时任务默认绑到「定时任务」工作区(目录 <默认目录>/tesk),也可自定义目录。
261
+ ensureWorkspace: (path, title) => ensureWorkspace(path, title),
262
+ // 先按「开」起来;settings 就绪后立刻按真实偏好对齐(见下面的 inject 回调)。
263
+ armed: true,
264
+ });
265
+ ctx.effect(() => () => scheduleEngine.dispose(), "t-team: schedule jobs");
266
+ ctx.reflect.provide(SCHEDULE_SERVICE, scheduleEngine);
267
+
268
+ /**
269
+ * 确保一个工作区存在,并返回 { id, title, path }。
270
+ *
271
+ * 顺序:目录不存在先 mkdir(宿主的工作区 create **只接受已存在的目录**)→ 交给
272
+ * workspaceRegistry.create:它内部对同路径工作区是**幂等复用**(createCanonical 第一件事
273
+ * 就是按 path 查找已有实体),所以重复调用不会造出第二个「定时任务」工作区。
274
+ *
275
+ * @param path - 目录绝对路径;空串表示用配置里的 scheduleWorkspaceDir。
276
+ * @param title - 工作区标题;缺省时:默认工作区用配置里的 scheduleWorkspaceTitle,自定义目录用目录名。
277
+ */
278
+ async function ensureWorkspace(path, title) {
279
+ const registry = ctx.get("workspaceRegistry") ?? ctx.get("workspace");
280
+ if (registry === undefined || typeof registry.create !== "function") {
281
+ throw new Error("宿主没有可用的工作区服务(workspaceRegistry.create),无法准备定时任务工作区。");
282
+ }
283
+ const raw = typeof path === "string" && path.trim() !== "" ? path.trim() : config.scheduleWorkspaceDir;
284
+ const custom = typeof path === "string" && path.trim() !== "";
285
+ const target = resolve(raw);
286
+ await mkdir(target, { recursive: true });
287
+ // 自定义目录的工作区标题缺省取目录名:否则几个自定义目录会挤成同名「定时任务」,侧栏里分不出来。
288
+ const label = title !== undefined && title !== ""
289
+ ? title
290
+ : (custom ? basename(target) : config.scheduleWorkspaceTitle);
291
+ const entity = await registry.create(target, label);
292
+ return { id: String(entity.id), title: String(entity.title ?? ""), path: String(entity.path ?? target) };
293
+ }
294
+
295
+ /**
296
+ * 把定时任务提示词里点名的专家落成**明确的召唤指令**(支持同时点名多位)。
297
+ *
298
+ * 面板的「T专家」挑选器往提示词开头填的就是 `@专家名`(可以连着填好几位);而执行时开的是一个
299
+ * **新会话**:它没有输入框那套引用芯片机制,只看到几个 `@名字`,模型不一定会去请人。所以这里按
300
+ * 当前名册把名字解析成"已启用专家",并把提示词改写成 summon_t_expert(1 位)/ summon_t_experts
301
+ * (多位)指令;解析不出来(名字不在已启用名册里)就**原样提交** —— 不猜,也不改写用户没指定的东西。
302
+ *
303
+ * 判据与面板完全一致(中文名 / 英文名 / slug):面板里填进去的名字,这里必须都认得。
304
+ *
305
+ * @param text - 任务里存的提示词原文。
306
+ * @returns 提交给新会话的文本。
307
+ */
308
+ async function expandSchedulePrompt(text) {
309
+ const raw = String(text ?? "");
310
+ if (!raw.includes("@")) return raw;
311
+ const current = await snapshot();
312
+ const enabled = new Set(current?.enabled ?? []);
313
+ const currentLocale = locale();
314
+ /** @type {{ name: string, display: string, slug: string }[]} */
315
+ const hits = [];
316
+ for (const expert of current?.experts ?? []) {
317
+ if (expert?.conflict === true || enabled.has(expert.slug) !== true) continue;
318
+ const names = [expert.name, expert.nameEn, expert.slug].filter((name) => typeof name === "string" && name !== "");
319
+ for (const name of names) {
320
+ if (!raw.includes(`@${name}`)) continue;
321
+ hits.push({
322
+ name,
323
+ display: currentLocale === "en" ? (expert.nameEn || expert.name) : (expert.name || expert.nameEn),
324
+ slug: expert.slug,
325
+ });
326
+ }
327
+ }
328
+ if (hits.length === 0) return raw;
329
+ // 长名先摘:名字互为子串时(「X」与「X 助理」)先处理长的,免得把长的截成短的;同一专家按 slug 去重。
330
+ hits.sort((a, b) => b.name.length - a.name.length);
331
+ const picked = [];
332
+ const seen = new Set();
333
+ for (const hit of hits) {
334
+ if (seen.has(hit.slug)) continue;
335
+ seen.add(hit.slug);
336
+ picked.push(hit);
337
+ }
338
+ let task = raw;
339
+ for (const hit of picked) task = task.split(`@${hit.name}`).join(" ");
340
+ task = task.replace(/[ \t]{2,}/gu, " ").trim();
341
+ if (picked.length === 1) {
342
+ return t(currentLocale, "schedule.expertInvoke", { name: picked[0].display, task });
343
+ }
344
+ const names = picked.map((hit) => hit.display).join(currentLocale === "en" ? ", " : "、");
345
+ return t(currentLocale, "schedule.expertInvokeMany", { names, task });
346
+ }
347
+
348
+ /**
349
+ * 把界面偏好里的「显示定时事项按钮」落到调度上。
350
+ *
351
+ * 为什么不是纯界面开关:用户口径是**关掉即功能整体停用** —— 只藏按钮却让 cron 继续在后台
352
+ * 开新会话干活,是最糟的一种"以为关了"。所以关掉就停掉全部定时触发,重开按 stored enabled 重建。
353
+ * @param prefs - 界面前缀(缺省视为开)。
354
+ */
355
+ function applyScheduleArmed(prefs) {
356
+ const armed = prefs?.showScheduleButton !== false;
357
+ scheduleEngine.setArmed(armed);
358
+ }
359
+
218
360
  // ---- 设置段(T专家 自己的命名空间,与其它插件不冲突)----
219
361
  // `settings` 是**可选**服务(见文件顶部的 inject 注释):宿主没挂它时本插件仍要能加载并工作,
220
362
  // 所以不在静态 `inject` 里。
@@ -245,7 +387,7 @@ export function apply(ctx, config) {
245
387
  return;
246
388
  }
247
389
  scope = settingsService.register(SETTINGS_NAMESPACE, settingsSchema, {
248
- base: { enabled: [] },
390
+ base: { enabled: [], showTaskBadge: true, showScheduleButton: true },
249
391
  applies: "live",
250
392
  validate: (value) => {
251
393
  const list = value?.enabled;
@@ -254,6 +396,9 @@ export function apply(ctx, config) {
254
396
  }
255
397
  },
256
398
  });
399
+ // 设置段一就绪就把定时事项开关对齐真实偏好:base 默认是「开」,而用户可能早就关过 ——
400
+ // 不对齐的话,插件每次启动都会先把已经关掉的功能重新跑起来。
401
+ applyScheduleArmed(scope?.get?.());
257
402
  });
258
403
  }
259
404
  const enabledSet = () => new Set(
@@ -737,10 +882,56 @@ export function apply(ctx, config) {
737
882
  }
738
883
  }
739
884
 
885
+ /**
886
+ * 读界面偏好(任务徽章 + 定时事项按钮)。
887
+ * 读当前值走 `scope.get()`(register 返回的句柄),与 enabledSet() 同一写法——
888
+ * 直接调 settingsService.get().value 在本机版本的设置服务上不成立(读到 undefined 抛错)。
889
+ *
890
+ * 两个键都**必有**且缺省为 true:设置文件里没有这个键时保持与默认一致,
891
+ * 客户端于是不必到处兜 undefined(wire schema 也是这么声明的)。
892
+ */
893
+ function getUiPrefs() {
894
+ const current = scope?.get?.();
895
+ return {
896
+ showTaskBadge: current?.showTaskBadge !== false,
897
+ showScheduleButton: current?.showScheduleButton !== false,
898
+ };
899
+ }
900
+
901
+ /** 只取 patch 里**确实是 boolean** 的键(其余一律忽略:坏值不该被写成"关")。 */
902
+ function normalizePrefsPatch(patch) {
903
+ const source = patch !== null && typeof patch === "object" ? patch : {};
904
+ return {
905
+ ...(typeof source.showTaskBadge === "boolean" ? { showTaskBadge: source.showTaskBadge } : {}),
906
+ ...(typeof source.showScheduleButton === "boolean" ? { showScheduleButton: source.showScheduleButton } : {}),
907
+ };
908
+ }
909
+
910
+ /**
911
+ * 写界面偏好(局部更新,不碰 enabled)。
912
+ *
913
+ * 写成功后再把「定时事项」那一位落到调度上:面板开关与后台定时器必须是同一个事实,
914
+ * 否则用户关了开关、cron 还在后台开新会话。
915
+ */
916
+ async function setUiPrefs(patch) {
917
+ const next = { ...getUiPrefs(), ...normalizePrefsPatch(patch) };
918
+ await settingsService.mutate(
919
+ SETTINGS_NAMESPACE,
920
+ [
921
+ { op: "set", path: ["showTaskBadge"], value: next.showTaskBadge },
922
+ { op: "set", path: ["showScheduleButton"], value: next.showScheduleButton },
923
+ ],
924
+ revision(),
925
+ );
926
+ applyScheduleArmed(next);
927
+ return getUiPrefs();
928
+ }
929
+
740
930
  const catalogService = {
741
931
  snapshot, prompt, setEnabled, root: config.root,
742
932
  createExpert, updateExpert, deleteExpert,
743
933
  createCategory, updateCategory, deleteCategory,
934
+ getUiPrefs, setUiPrefs,
744
935
  };
745
936
  ctx.reflect.provide(CATALOG_SERVICE, catalogService);
746
937
 
@@ -73,6 +73,23 @@ export const enabledStateSchema = z.object({
73
73
  revision: z.number().int().min(0),
74
74
  });
75
75
 
76
+ /**
77
+ * 界面偏好(宿主持久化,客户端只读展示 + 写入)。
78
+ *
79
+ * 两个键都是 `boolean` 且**必有**:host 侧读取时缺省一律补 true,客户端因此不必到处兜 undefined。
80
+ * 加新键要同时改 host(lib/index.js 的 settingsSchema 与 getUiPrefs)与客户端消费处。
81
+ */
82
+ export const uiPrefsSchema = z.object({
83
+ showTaskBadge: z.boolean(),
84
+ showScheduleButton: z.boolean(),
85
+ });
86
+
87
+ /** `setUiPrefs` 的入参:想改哪个传哪个(未传的键保持原值)。 */
88
+ export const uiPrefsPatchSchema = z.object({
89
+ showTaskBadge: z.boolean().optional(),
90
+ showScheduleButton: z.boolean().optional(),
91
+ });
92
+
76
93
  export const promptSchema = z.object({ prompt: z.string() });
77
94
 
78
95
  export const squadMemberSchema = z.object({
@@ -154,3 +171,68 @@ export const squadInputSchema = z.object({
154
171
  enabled: z.boolean(),
155
172
  members: z.array(z.string()),
156
173
  });
174
+
175
+ // ---------------------------------------------------------------- 定时事项
176
+ // 与 @weibaohui/dsh-tasks 的线格式**刻意不同名**:两个插件可以同时安装,各自的 schema / 服务名 /
177
+ // 数据文件互不相干,谁先加载都不会影响对方。
178
+
179
+ /** 一次执行尝试:什么时候跑的、开出哪个会话、成败与失败原因。 */
180
+ export const scheduleRunSchema = z.object({
181
+ at: z.string(),
182
+ ok: z.boolean(),
183
+ sessionId: z.string().optional(),
184
+ error: z.string().optional(),
185
+ });
186
+
187
+ /** 一条定时事项。 */
188
+ export const scheduleItemSchema = z.object({
189
+ id: z.string(),
190
+ title: z.string(),
191
+ prompt: z.string(),
192
+ cron: z.string(),
193
+ enabled: z.boolean(),
194
+ workspaceId: z.string().optional(),
195
+ createdAt: z.string(),
196
+ updatedAt: z.string(),
197
+ lastRunAt: z.string().optional(),
198
+ lastRunError: z.string().optional(),
199
+ runs: z.array(scheduleRunSchema),
200
+ });
201
+
202
+ /** 面板一次性拿全的数据:事项 + 开关 + 工作区选项 + 宿主能力探测。 */
203
+ export const scheduleSnapshotSchema = z.object({
204
+ items: z.array(scheduleItemSchema),
205
+ /** 定时事项功能是否启用(= 设置页那个开关的当前值)。 */
206
+ armed: z.boolean(),
207
+ /** 数据文件绝对路径(面板把它显示在说明里,便于用户备份)。 */
208
+ file: z.string(),
209
+ /** 正在执行中的事项 id(面板据此显示「执行中…」)。 */
210
+ running: z.array(z.string()),
211
+ capabilities: z.object({ agents: z.boolean(), presets: z.boolean() }),
212
+ /** 可选工作区(下拉选项);path 是目录,面板显示给用户看落地位置。 */
213
+ workspaces: z.array(z.object({ id: z.string(), title: z.string(), path: z.string().optional() })),
214
+ });
215
+
216
+ /** 新建/编辑表单提交的字段。 */
217
+ export const scheduleInputSchema = z.object({
218
+ title: z.string().min(1).max(200),
219
+ prompt: z.string().min(1).max(20000),
220
+ cron: z.string().min(1).max(120),
221
+ enabled: z.boolean(),
222
+ /** 空串 = 不绑定工作区(显式解绑也走这个值)。 */
223
+ workspaceId: z.string().max(200).optional(),
224
+ });
225
+
226
+ /** 编辑时的局部更新(未传的键保持原值)。 */
227
+ export const schedulePatchSchema = scheduleInputSchema.partial();
228
+
229
+ /** `previewSchedule` 的结果:接下来几次触发时间(ISO)。 */
230
+ export const schedulePreviewSchema = z.object({ fires: z.array(z.string()) });
231
+
232
+ /** `setArmed` 的结果。 */
233
+ export const scheduleArmedSchema = z.object({ armed: z.boolean() });
234
+
235
+ /** `ensureScheduleWorkspace` 的返回:定时任务要绑定的工作区(默认「定时任务」工作区或自定义目录)。 */
236
+ export const scheduleWorkspaceSchema = z.object({
237
+ workspace: z.object({ id: z.string(), title: z.string(), path: z.string() }),
238
+ });
package/lib/remote.js CHANGED
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { Remote, RemoteError, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
9
9
  import { z } from "zod";
10
- import { CATALOG_SERVICE, SETTINGS_NAMESPACE, SQUAD_SERVICE, TEAM_SERVICE, UNKNOWN_SLUG_ERROR_CODE } from "./index.js";
10
+ import { CATALOG_SERVICE, SCHEDULE_SERVICE, SETTINGS_NAMESPACE, SQUAD_SERVICE, TEAM_SERVICE, UNKNOWN_SLUG_ERROR_CODE } from "./index.js";
11
11
 
12
12
  /**
13
13
  * 领域码形态:`<domain>/<reason>`(如 `tTeam/squads-invalid`)。
@@ -29,6 +29,11 @@ import {
29
29
  expertSchema,
30
30
  promptSchema,
31
31
  rosterExpertSchema,
32
+ scheduleInputSchema,
33
+ schedulePatchSchema,
34
+ schedulePreviewSchema,
35
+ scheduleWorkspaceSchema,
36
+ scheduleSnapshotSchema,
32
37
  squadInputSchema,
33
38
  squadMemberSchema,
34
39
  squadSchema,
@@ -38,6 +43,8 @@ import {
38
43
  teamMemberSchema,
39
44
  teamSchema,
40
45
  teamsSnapshotSchema,
46
+ uiPrefsPatchSchema,
47
+ uiPrefsSchema,
41
48
  } from "./remote-schemas.js";
42
49
 
43
50
 
@@ -56,6 +63,73 @@ function descriptor(method, parameters, typeSymbol, schema) {
56
63
 
57
64
  const DESCRIPTORS = [
58
65
  descriptor("getCatalog", [], "TTeamCatalog", catalogSnapshotSchema),
66
+ descriptor("getUiPrefs", [], "TTeamUiPrefs", uiPrefsSchema),
67
+ // 入参从「单个 boolean」改成 patch 对象:界面偏好不再只有任务徽章一项,
68
+ // 加键不必再加方法(未传的键保持原值,host 侧做兜底)。
69
+ descriptor("setUiPrefs", [
70
+ {
71
+ name: "patch",
72
+ wire: "patch",
73
+ source: "json",
74
+ codec: { mode: "strict", typeSymbol: "TTeamUiPrefsPatch", schema: uiPrefsPatchSchema },
75
+ },
76
+ ], "TTeamUiPrefs", uiPrefsSchema),
77
+ // 定时事项:全部写操作都回传刷新后的整份快照,面板拿到就能直接重绘(与名册/小队同一口径)。
78
+ descriptor("getSchedule", [], "TTeamSchedule", scheduleSnapshotSchema),
79
+ descriptor("createScheduleItem", [
80
+ {
81
+ name: "input",
82
+ wire: "input",
83
+ source: "json",
84
+ codec: { mode: "strict", typeSymbol: "TTeamScheduleInput", schema: scheduleInputSchema },
85
+ },
86
+ ], "TTeamSchedule", scheduleSnapshotSchema),
87
+ descriptor("updateScheduleItem", [
88
+ {
89
+ name: "id",
90
+ wire: "id",
91
+ source: "json",
92
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128) },
93
+ },
94
+ {
95
+ name: "patch",
96
+ wire: "patch",
97
+ source: "json",
98
+ codec: { mode: "strict", typeSymbol: "TTeamSchedulePatch", schema: schedulePatchSchema },
99
+ },
100
+ ], "TTeamSchedule", scheduleSnapshotSchema),
101
+ descriptor("deleteScheduleItem", [
102
+ {
103
+ name: "id",
104
+ wire: "id",
105
+ source: "json",
106
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128) },
107
+ },
108
+ ], "TTeamSchedule", scheduleSnapshotSchema),
109
+ descriptor("runScheduleItem", [
110
+ {
111
+ name: "id",
112
+ wire: "id",
113
+ source: "json",
114
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(128) },
115
+ },
116
+ ], "TTeamSchedule", scheduleSnapshotSchema),
117
+ descriptor("previewSchedule", [
118
+ {
119
+ name: "cron",
120
+ wire: "cron",
121
+ source: "json",
122
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().min(1).max(120) },
123
+ },
124
+ ], "TTeamSchedulePreview", schedulePreviewSchema),
125
+ descriptor("ensureScheduleWorkspace", [
126
+ {
127
+ name: "path",
128
+ wire: "path",
129
+ source: "json",
130
+ codec: { mode: "strict", typeSymbol: "string", schema: z.string().max(1024) },
131
+ },
132
+ ], "TTeamScheduleWorkspace", scheduleWorkspaceSchema),
59
133
  descriptor("setEnabled", [
60
134
  {
61
135
  name: "enabled",
@@ -287,6 +361,111 @@ class TTeamRemote extends TypertRemoteService {
287
361
  }
288
362
 
289
363
  /** 整体替换启用列表;过期修订号由 settings 服务拒绝。 */
364
+ async getUiPrefs() {
365
+ try {
366
+ return await this.catalog().getUiPrefs();
367
+ } catch (error) {
368
+ throw this.businessError(error, "tTeam/ui-prefs-unreadable");
369
+ }
370
+ }
371
+
372
+ async setUiPrefs(patch) {
373
+ try {
374
+ return await this.catalog().setUiPrefs(patch);
375
+ } catch (error) {
376
+ throw this.businessError(error, "tTeam/ui-prefs-unwritable");
377
+ }
378
+ }
379
+
380
+ /**
381
+ * 定时事项服务(T专家 自己的引擎,与 @weibaohui/dsh-tasks 完全独立)。
382
+ *
383
+ * 与 `teams()` 同理:缺服务时给的是**功能语境的**错误码(`tTeam/schedule-unavailable`
384
+ * 读起来就是「定时事项用不了」),而不是笼统的 host-unavailable。
385
+ */
386
+ schedule() {
387
+ const service = this.ctx.get(SCHEDULE_SERVICE);
388
+ if (service === undefined) {
389
+ // @ts-expect-error 上游类型缺口:同上,tTeam/* 业务码不在闭合的 RemoteErrorDetailsMap 里。
390
+ throw new RemoteError("tTeam/schedule-unavailable", "T专家 定时事项服务不可用(host 插件未加载或已卸载)", { service: SCHEDULE_SERVICE });
391
+ }
392
+ return service;
393
+ }
394
+
395
+ /** 定时事项快照:事项列表 + 开关状态 + 工作区选项 + 宿主能力探测。 */
396
+ async getSchedule() {
397
+ try {
398
+ return this.schedule().snapshot();
399
+ } catch (error) {
400
+ throw this.businessError(error, "tTeam/schedule-unreadable");
401
+ }
402
+ }
403
+
404
+ /** 新建一条定时事项。 */
405
+ async createScheduleItem(input) {
406
+ try {
407
+ const service = this.schedule();
408
+ await service.create(input);
409
+ return service.snapshot();
410
+ } catch (error) {
411
+ throw this.businessError(error, "tTeam/schedule-invalid");
412
+ }
413
+ }
414
+
415
+ /** 改一条定时事项(局部更新)。 */
416
+ async updateScheduleItem(id, patch) {
417
+ try {
418
+ const service = this.schedule();
419
+ await service.update(id, patch);
420
+ return service.snapshot();
421
+ } catch (error) {
422
+ throw this.businessError(error, "tTeam/schedule-invalid");
423
+ }
424
+ }
425
+
426
+ /** 删一条定时事项。 */
427
+ async deleteScheduleItem(id) {
428
+ try {
429
+ const service = this.schedule();
430
+ await service.remove(id);
431
+ return service.snapshot();
432
+ } catch (error) {
433
+ throw this.businessError(error, "tTeam/schedule-invalid");
434
+ }
435
+ }
436
+
437
+ /** 立即执行一条定时事项(等价于等它到点)。 */
438
+ async runScheduleItem(id) {
439
+ try {
440
+ const service = this.schedule();
441
+ await service.runNow(id);
442
+ return service.snapshot();
443
+ } catch (error) {
444
+ throw this.businessError(error, "tTeam/schedule-run-failed");
445
+ }
446
+ }
447
+
448
+ /**
449
+ * 确保定时任务要绑定的工作区存在:空 path = 配置里的默认「定时任务」工作区(目录 <默认目录>/tesk);
450
+ * 给了 path = 面板里填的自定义目录(不存在会建出来)。宿主的工作区 create 对同路径是幂等复用。
451
+ */
452
+ async ensureScheduleWorkspace(path) {
453
+ try {
454
+ return { workspace: await this.schedule().ensureWorkspace(String(path ?? ""), undefined) };
455
+ } catch (error) {
456
+ throw this.businessError(error, "tTeam/schedule-workspace-failed");
457
+ }
458
+ }
459
+
460
+ /** 预览某个 cron 表达式接下来的 5 次触发时间(宿主用 croner 算,客户端不打包 cron 解析器)。 */
461
+ async previewSchedule(cron) {
462
+ try {
463
+ return this.schedule().preview(cron);
464
+ } catch (error) {
465
+ throw this.businessError(error, "tTeam/schedule-invalid");
466
+ }
467
+ }
468
+
290
469
  async setEnabled(enabled, expectedRevision) {
291
470
  try {
292
471
  return await this.catalog().setEnabled(enabled, expectedRevision);
@@ -480,6 +659,8 @@ class TTeamRemote extends TypertRemoteService {
480
659
 
481
660
  exposeRemoteMethods(TTeamRemote, [
482
661
  "getCatalog", "setEnabled", "getPrompt", "getSquads", "saveSquads", "getTeams", "stopTeam", "startSquad",
662
+ "getUiPrefs", "setUiPrefs",
663
+ "getSchedule", "createScheduleItem", "updateScheduleItem", "deleteScheduleItem", "runScheduleItem", "previewSchedule", "ensureScheduleWorkspace",
483
664
  "createExpert", "updateExpert", "deleteExpert",
484
665
  "createCategory", "updateCategory", "deleteCategory",
485
666
  ]);