dsh-plugin-t-expert 0.2.9 → 0.2.12

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/plan-check.js CHANGED
@@ -1,44 +1,19 @@
1
+ // @ts-check
1
2
  /**
2
3
  * DAG 预检(只读):把「打算创建的一整套任务」先跑一遍引擎的校验,**一次报出全部问题**。
3
4
  *
4
5
  * 为什么需要它:引擎建任务是**逐条**的(依赖只能引用已存在的任务),每条硬校验都单独拒绝,
5
6
  * 中途失败会留下半成品,模型再重试就容易出现重复任务(本机实测踩过)。
6
- * 这里复用引擎自己的 `validateCreateTask`(契约/写域重叠/质量门槛),再补上引擎在
7
- * `validateStagedGraph`(未导出)里做的引用与环检查,全部在内存里模拟、不写任何状态。
7
+ * 这里复用引擎自己的 `validateCreateTask`(契约/写域重叠/质量门槛),**并复用引擎导出的
8
+ * `collectStagedGraphProblems`** 做引用与环检查 —— 全部在内存里模拟、不写任何状态。
9
+ *
10
+ * 规则**只有一份**(`lib/teams/tools.js`):过去这里按同规则复刻过一遍,两处一旦漂移,
11
+ * 预检就会说 ok、真建任务却被引擎拒 —— 而预检的全部意义正是提前发现这种岔路。
8
12
  */
9
- import { CAPTAIN_KEY, readTeam } from "./teams/state.js";
13
+ import { CAPTAIN_KEY } from "./teams/state.js";
10
14
  import { normalizeAssigneeForCreate } from "./teams/assignee-contract.js";
11
15
  import { validateCreateTask } from "./teams/quality-gates.js";
12
-
13
- /** 与引擎 `validateStagedGraph` 对齐的引用/环检查(该函数未导出,这里按同规则复刻)。 */
14
- function graphProblems(tasks) {
15
- const problems = [];
16
- const ids = new Set(tasks.map((task) => task.id));
17
- for (const task of tasks) {
18
- if (String(task.subject ?? "").trim() === "") problems.push(`任务 ${task.id} 缺少 subject`);
19
- for (const dependency of task.dependencies ?? []) {
20
- if (dependency === task.id) problems.push(`任务 ${task.id} 不能依赖自己`);
21
- else if (!ids.has(dependency)) problems.push(`任务 ${task.id} 依赖不存在的 ${dependency}`);
22
- }
23
- }
24
- // 环路检测(DFS 三色)
25
- const visiting = new Set();
26
- const visited = new Set();
27
- const byId = new Map(tasks.map((task) => [task.id, task]));
28
- const walk = (id, trail) => {
29
- if (visiting.has(id)) {
30
- problems.push(`依赖成环:${[...trail, id].join(" → ")}`);
31
- return;
32
- }
33
- if (visited.has(id)) return;
34
- visiting.add(id);
35
- for (const dependency of byId.get(id)?.dependencies ?? []) walk(dependency, [...trail, id]);
36
- visiting.delete(id);
37
- visited.add(id);
38
- };
39
- for (const task of tasks) walk(task.id, []);
40
- return [...new Set(problems)];
41
- }
16
+ import { collectStagedGraphProblems } from "./teams/tools.js";
42
17
 
43
18
  /** 拓扑序(依赖在前);有环时返回已排好的部分。 */
44
19
  function topologicalOrder(tasks) {
@@ -77,32 +52,46 @@ export function checkPlan(team, tasks) {
77
52
  // 逐条模拟:先做静态检查,再跑引擎的 validateCreateTask(它只看已存在的任务 + 已接受的模拟任务)
78
53
  const sim = { ...(team ?? {}), tasks: [...existing] };
79
54
  const taken = new Set(existing.map((task) => task.id));
80
- const labels = new Map(); // 本批 id 模拟 id
55
+ // 本批 id(标签)→ 引擎会分配的模拟 id。**按声明顺序累积**,不预先解析全批 —— 见
56
+ // `resolveDependencies` 的说明:引擎要求依赖已存在,前向引用本就建不出来。
57
+ const labels = new Map();
58
+ // 每个位置(含**没有 id 的**任务)→ 模拟 id:发号与标签解析必须共用同一次发号,
59
+ // 否则「只给带 id 的发号」会让报告里的 id 与实际创建顺序不一致。
60
+ const simulatedIds = [];
81
61
  // 引擎分配的是 `t${team.taskSeq + 1}` 并随后自增(不是"现有任务条数")。两者只在
82
62
  // taskSeq === tasks.length 时才一致 —— 有任务被删除/取消过就会分叉,所以以 taskSeq 为准。
83
63
  let counter = Number.isSafeInteger(team?.taskSeq) ? team.taskSeq : existing.length;
84
- const report = [];
85
-
86
- proposed.forEach((raw, index) => {
87
- const local = [];
88
- const wantId = typeof raw?.id === "string" && raw.id.trim() !== "" ? raw.id.trim() : undefined;
64
+ const issueSimulatedId = () => {
89
65
  let simulatedId;
90
66
  do {
91
67
  counter += 1;
92
68
  simulatedId = `t${counter}`;
93
- } while (taken.has(simulatedId));
69
+ } while (taken.has(simulatedId) || existing.some((task) => task.id === simulatedId));
94
70
  taken.add(simulatedId);
71
+ return simulatedId;
72
+ };
73
+ const resolveDependencies = (raw) => (Array.isArray(raw?.dependencies) ? raw.dependencies : [])
74
+ .map((dependency) => String(dependency))
75
+ .map((dependency) => labels.get(dependency) ?? dependency);
76
+ const report = [];
77
+
78
+ proposed.forEach((raw, index) => {
79
+ const local = [];
80
+ const wantId = typeof raw?.id === "string" && raw.id.trim() !== "" ? raw.id.trim() : undefined;
81
+ const simulatedId = issueSimulatedId();
82
+ simulatedIds[index] = simulatedId;
83
+ // 登记在本条**解析依赖之后**:这样 `dependencies` 里的同批标签只有「已声明过的」
84
+ // 才能解析成模拟 id,与引擎的「依赖必须已存在」一致。
95
85
  if (wantId !== undefined) labels.set(wantId, simulatedId);
96
86
 
97
87
  const subject = String(raw?.subject ?? "").trim();
98
88
  if (subject === "") local.push("缺少 subject");
99
89
  const kind = typeof raw?.kind === "string" && raw.kind !== "" ? raw.kind : "work";
100
- const dependencies = (Array.isArray(raw?.dependencies) ? raw.dependencies : [])
101
- .map((dependency) => String(dependency))
102
- .map((dependency) => labels.get(dependency) ?? dependency);
90
+ const dependencies = resolveDependencies(raw);
103
91
  if (dependencies.includes(simulatedId)) local.push("不能依赖自己");
92
+ // 判定「不存在的依赖」时,本批**已声明**的标签算作已知(它们会在模拟里成为真实任务 id)。
104
93
  const unknown = dependencies.filter(
105
- (dependency) => !taken.has(dependency) && !existing.some((task) => task.id === dependency),
94
+ (dependency) => !taken.has(dependency) && !labels.has(dependency) && !existing.some((task) => task.id === dependency),
106
95
  );
107
96
  if (unknown.length > 0) local.push(`依赖不存在的任务:${unknown.join(", ")}`);
108
97
  const rawAssignee = typeof raw?.assignee === "string" && raw.assignee.trim() !== "" ? raw.assignee.trim() : undefined;
@@ -142,8 +131,9 @@ export function checkPlan(team, tasks) {
142
131
  report.push({ index, id: wantId ?? simulatedId, simulatedId, subject, ok: local.length === 0, error: local.join(";") });
143
132
  });
144
133
 
145
- // 环检查(含已存在的任务)
146
- problems.push(...graphProblems(sim.tasks).map((error) => ({ index: -1, id: "graph", subject: "", error })));
134
+ // 环/引用检查(含已存在的任务):**调用引擎导出实现**,不再本地复刻。
135
+ // requireRunnable=false —— 预检允许"成员还没加",那是 approve 阶段才要求的。
136
+ problems.push(...collectStagedGraphProblems(sim, false).map((error) => ({ index: -1, id: "graph", subject: "", error })));
147
137
 
148
138
  // 提示(不阻塞):成员同时只能有 1 个未完成任务(引擎在 claim/reassign 时才会拒)
149
139
  const openStatuses = new Set(["pending", "claimed", "in_progress"]);
@@ -174,11 +164,3 @@ export function checkPlan(team, tasks) {
174
164
  existing: existing.map((task) => ({ id: task.id, status: task.status, kind: task.kind ?? "work", assignee: task.assignee ?? "" })),
175
165
  };
176
166
  }
177
-
178
- /** 从磁盘读当前团队(captain 会话 → 团队)。 */
179
- export async function readCaptainTeam({ stateRoot, captainSessionId, findTeamByCaptain }) {
180
- const team = await findTeamByCaptain(stateRoot, captainSessionId);
181
- return team;
182
- }
183
-
184
- export { readTeam };
@@ -0,0 +1,156 @@
1
+ // @ts-check
2
+ /**
3
+ * Remote 线格式的**单一 schema 源**(A-2 / A-3)。
4
+ *
5
+ * 为什么要有这个文件:这些 schema 过去在 host(`lib/remote.js`)与客户端(`src/client.jsx`)
6
+ * 各写了一份,**13 个同名定义逐字重复**。加一个 remote 方法或改一个字段要改两处,漏改一处
7
+ * 不会在加载期报错,只会在调用期炸(或更糟:静默把字段剥掉)。
8
+ *
9
+ * 权威在 **host**:它同时是生产方与校验方(`lib/remote.js` 的 DESCRIPTORS 用的就是这些对象),
10
+ * 客户端只是消费方。所以统一后 host 的校验强度不变,客户端变严——方向是安全的。
11
+ * 统一过程中修正了两处真实不一致:
12
+ * 1. `squadInputSchema.key` 客户端原来没有 `min(1).max(64)`;
13
+ * 2. 客户端原来把团队快照叫 `catalogSchema`、把 `teamMemberSchema` 内联写了一遍。
14
+ *
15
+ * ⚠️ 本文件只放**纯 schema**,不放 host 专属的 `descriptor()` 或客户端专属的 `direct()` 信封
16
+ * —— 两端信封的字段(`id` 前缀、`result` 形状)本来就不同,强行合并反而更脆。
17
+ */
18
+ import { z } from "zod";
19
+
20
+ export const expertSchema = z.object({
21
+ slug: z.string(),
22
+ name: z.string(),
23
+ nameEn: z.string(),
24
+ description: z.string(),
25
+ descriptionEn: z.string(),
26
+ emoji: z.string(),
27
+ division: z.string(),
28
+ divisionZh: z.string(),
29
+ divisionEn: z.string(),
30
+ conflict: z.boolean().optional(),
31
+ /** 用户自建专家(可编辑/可删除);内置专家不带此标记。 */
32
+ custom: z.boolean().optional(),
33
+ /** 自建专家文件指纹:编辑/删除时回传,用于拦住并发覆盖。 */
34
+ hash: z.string().optional(),
35
+ });
36
+
37
+ /** 新建/编辑自建专家的入参(面板表单字段)。 */
38
+ export const customExpertInputSchema = z.object({
39
+ slug: z.string().min(1).max(128).optional(),
40
+ name: z.string().min(1).max(200),
41
+ nameEn: z.string().max(200).optional(),
42
+ description: z.string().min(1).max(1000),
43
+ descriptionEn: z.string().max(1000).optional(),
44
+ emoji: z.string().max(16).optional(),
45
+ body: z.string().min(1).max(60000),
46
+ /** 落点分区(自建分区目录名);留空=默认分区。服务端仍会自己校验一遍。 */
47
+ division: z.string().max(128).optional(),
48
+ /** 新建分区时的中文显示名(落在 <customRoot>/divisions.json)。 */
49
+ divisionLabel: z.string().max(40).optional(),
50
+ });
51
+
52
+ /** `getCatalog` 及分类写回方法的返回快照。 */
53
+ export const catalogSnapshotSchema = z.object({
54
+ experts: z.array(expertSchema),
55
+ enabled: z.array(z.string()),
56
+ revision: z.number().int().min(0),
57
+ // 这两个字段必须声明:zod 会把 schema 里没有的键**直接剥掉**(实测),
58
+ // 漏声明时面板拿到的快照里根本没有它们,界面上表现为"分区标签是空的"。
59
+ customDivision: z.string().optional(),
60
+ customDivisionLabel: z.string().optional(),
61
+ /** 可选的落点分类(面板「分类」下拉与管理页):官方在前,其余按目录/声明发现。 */
62
+ categories: z.array(z.object({
63
+ key: z.string(),
64
+ label: z.string(),
65
+ official: z.boolean(),
66
+ count: z.number().int().min(0),
67
+ customCount: z.number().int().min(0),
68
+ })).optional(),
69
+ });
70
+
71
+ export const enabledStateSchema = z.object({
72
+ enabled: z.array(z.string()),
73
+ revision: z.number().int().min(0),
74
+ });
75
+
76
+ export const promptSchema = z.object({ prompt: z.string() });
77
+
78
+ export const squadMemberSchema = z.object({
79
+ slug: z.string(),
80
+ role: z.string(),
81
+ name: z.string(),
82
+ nameZh: z.string(),
83
+ division: z.string(),
84
+ divisionZh: z.string(),
85
+ known: z.boolean(),
86
+ });
87
+
88
+ export const squadSchema = z.object({
89
+ key: z.string(),
90
+ description: z.string(),
91
+ aliases: z.array(z.string()),
92
+ taskPlanning: z.enum(["captain", "seed"]),
93
+ enabled: z.boolean(),
94
+ members: z.array(squadMemberSchema),
95
+ });
96
+
97
+ export const rosterExpertSchema = z.object({
98
+ slug: z.string(),
99
+ name: z.string(),
100
+ nameZh: z.string(),
101
+ division: z.string(),
102
+ divisionZh: z.string(),
103
+ enabled: z.boolean(),
104
+ conflict: z.boolean(),
105
+ });
106
+
107
+ export const squadsSnapshotSchema = z.object({
108
+ revision: z.string(),
109
+ squads: z.array(squadSchema),
110
+ experts: z.array(rosterExpertSchema),
111
+ lastBuild: z.object({ at: z.number(), ok: z.boolean(), output: z.string() }),
112
+ });
113
+
114
+ export const teamMemberSchema = z.object({
115
+ name: z.string(),
116
+ role: z.string(),
117
+ status: z.string(),
118
+ activity: z.string(),
119
+ done: z.number(),
120
+ total: z.number(),
121
+ });
122
+
123
+ export const teamSchema = z.object({
124
+ teamId: z.string(),
125
+ name: z.string(),
126
+ workspace: z.string(),
127
+ captainSessionId: z.string(),
128
+ phase: z.string(),
129
+ halted: z.boolean(),
130
+ members: z.array(teamMemberSchema),
131
+ tasks: z.array(z.object({ id: z.string(), subject: z.string(), status: z.string(), assignee: z.string() })),
132
+ });
133
+
134
+ export const teamsSnapshotSchema = z.object({ teams: z.array(teamSchema) });
135
+
136
+ export const startResultSchema = z.object({
137
+ profile: z.string(),
138
+ label: z.string(),
139
+ members: z.number(),
140
+ });
141
+
142
+ export const stopResultSchema = z.object({
143
+ teamName: z.string(),
144
+ cancelledTasks: z.number(),
145
+ alreadyHalted: z.boolean(),
146
+ });
147
+
148
+ /** 面板提交的小队定义(saveSquads 的入参)。 */
149
+ export const squadInputSchema = z.object({
150
+ key: z.string().min(1).max(64),
151
+ description: z.string(),
152
+ aliases: z.array(z.string()),
153
+ taskPlanning: z.enum(["captain", "seed"]),
154
+ enabled: z.boolean(),
155
+ members: z.array(z.string()),
156
+ });
package/lib/remote.js CHANGED
@@ -1,3 +1,4 @@
1
+ // @ts-check
1
2
  /**
2
3
  * T专家 remote 服务:把 host 侧的名册/启用状态/persona 通过 Typert Gateway 暴露给客户端面板。
3
4
  *
@@ -19,140 +20,26 @@ const DOMAIN_CODE_PATTERN = /^[a-zA-Z][a-zA-Z0-9]*\/[a-z0-9-]+$/u;
19
20
  /** 与 `lib/index.js` 共用的「未知专家 slug」稳定码(不用显示文案做分类,见 D-3)。 */
20
21
  const UNKNOWN_SLUG_CODE = UNKNOWN_SLUG_ERROR_CODE;
21
22
 
22
- const expertSchema = z.object({
23
- slug: z.string(),
24
- name: z.string(),
25
- nameEn: z.string(),
26
- description: z.string(),
27
- descriptionEn: z.string(),
28
- emoji: z.string(),
29
- division: z.string(),
30
- divisionZh: z.string(),
31
- divisionEn: z.string(),
32
- conflict: z.boolean().optional(),
33
- /** 用户自建专家(可编辑/可删除);内置专家不带此标记。 */
34
- custom: z.boolean().optional(),
35
- /** 自建专家文件指纹:编辑/删除时回传,用于拦住并发覆盖。 */
36
- hash: z.string().optional(),
37
- });
23
+ // A-2/A-3:线格式 schema 的**单一源**在 ./remote-schemas.js(客户端也从这里取),
24
+ // 不再在本文件重复定义一份。host 仍是权威(本文件是生产方与校验方)。
25
+ import {
26
+ catalogSnapshotSchema,
27
+ customExpertInputSchema,
28
+ enabledStateSchema,
29
+ expertSchema,
30
+ promptSchema,
31
+ rosterExpertSchema,
32
+ squadInputSchema,
33
+ squadMemberSchema,
34
+ squadSchema,
35
+ squadsSnapshotSchema,
36
+ startResultSchema,
37
+ stopResultSchema,
38
+ teamMemberSchema,
39
+ teamSchema,
40
+ teamsSnapshotSchema,
41
+ } from "./remote-schemas.js";
38
42
 
39
- /** 新建/编辑自建专家的入参(面板表单字段)。 */
40
- const customExpertInputSchema = z.object({
41
- slug: z.string().min(1).max(128).optional(),
42
- name: z.string().min(1).max(200),
43
- nameEn: z.string().max(200).optional(),
44
- description: z.string().min(1).max(1000),
45
- descriptionEn: z.string().max(1000).optional(),
46
- emoji: z.string().max(16).optional(),
47
- body: z.string().min(1).max(60000),
48
- /** 落点分区(自建分区目录名);留空=默认分区。服务端仍会自己校验一遍。 */
49
- division: z.string().max(128).optional(),
50
- /** 新建分区时的中文显示名(落在 <customRoot>/divisions.json)。 */
51
- divisionLabel: z.string().max(40).optional(),
52
- });
53
-
54
- const catalogSnapshotSchema = z.object({
55
- experts: z.array(expertSchema),
56
- enabled: z.array(z.string()),
57
- revision: z.number().int().min(0),
58
- // 这两个字段必须声明:zod 会把 schema 里没有的键**直接剥掉**(实测),
59
- // 漏声明时面板拿到的快照里根本没有它们,界面上表现为"分区标签是空的"。
60
- customDivision: z.string().optional(),
61
- customDivisionLabel: z.string().optional(),
62
- /** 可选的落点分类(面板「分类」下拉与管理页):官方在前,其余按目录/声明发现。 */
63
- categories: z.array(z.object({
64
- key: z.string(),
65
- label: z.string(),
66
- official: z.boolean(),
67
- count: z.number().int().min(0),
68
- customCount: z.number().int().min(0),
69
- })).optional(),
70
- });
71
-
72
- const enabledStateSchema = z.object({
73
- enabled: z.array(z.string()),
74
- revision: z.number().int().min(0),
75
- });
76
-
77
- const promptSchema = z.object({ prompt: z.string() });
78
-
79
- const squadMemberSchema = z.object({
80
- slug: z.string(),
81
- role: z.string(),
82
- name: z.string(),
83
- nameZh: z.string(),
84
- division: z.string(),
85
- divisionZh: z.string(),
86
- known: z.boolean(),
87
- });
88
-
89
- const squadSchema = z.object({
90
- key: z.string(),
91
- description: z.string(),
92
- aliases: z.array(z.string()),
93
- taskPlanning: z.enum(["captain", "seed"]),
94
- enabled: z.boolean(),
95
- members: z.array(squadMemberSchema),
96
- });
97
-
98
- const rosterExpertSchema = z.object({
99
- slug: z.string(),
100
- name: z.string(),
101
- nameZh: z.string(),
102
- division: z.string(),
103
- divisionZh: z.string(),
104
- enabled: z.boolean(),
105
- conflict: z.boolean(),
106
- });
107
-
108
- const squadsSnapshotSchema = z.object({
109
- revision: z.string(),
110
- squads: z.array(squadSchema),
111
- experts: z.array(rosterExpertSchema),
112
- lastBuild: z.object({ at: z.number(), ok: z.boolean(), output: z.string() }),
113
- });
114
-
115
- const teamMemberSchema = z.object({
116
- name: z.string(),
117
- role: z.string(),
118
- status: z.string(),
119
- activity: z.string(),
120
- done: z.number(),
121
- total: z.number(),
122
- });
123
-
124
- const teamSchema = z.object({
125
- teamId: z.string(),
126
- name: z.string(),
127
- workspace: z.string(),
128
- captainSessionId: z.string(),
129
- phase: z.string(),
130
- halted: z.boolean(),
131
- members: z.array(teamMemberSchema),
132
- tasks: z.array(z.object({ id: z.string(), subject: z.string(), status: z.string(), assignee: z.string() })),
133
- });
134
-
135
- const teamsSnapshotSchema = z.object({ teams: z.array(teamSchema) });
136
- const startResultSchema = z.object({
137
- profile: z.string(),
138
- label: z.string(),
139
- members: z.number(),
140
- });
141
- const stopResultSchema = z.object({
142
- teamName: z.string(),
143
- cancelledTasks: z.number(),
144
- alreadyHalted: z.boolean(),
145
- });
146
-
147
- /** 面板提交的小队定义(saveSquads 的入参)。 */
148
- const squadInputSchema = z.object({
149
- key: z.string().min(1).max(64),
150
- description: z.string(),
151
- aliases: z.array(z.string()),
152
- taskPlanning: z.enum(["captain", "seed"]),
153
- enabled: z.boolean(),
154
- members: z.array(z.string()),
155
- });
156
43
 
157
44
  /** 一个 direct 调用描述符。 */
158
45
  function descriptor(method, parameters, typeSymbol, schema) {
@@ -348,6 +235,8 @@ function exposeRemoteMethods(Class, methods) {
348
235
  access: { has: (object) => method in object, get: (object) => object[method] },
349
236
  addInitializer: (initializer) => initializer.call(Object.create(Class.prototype)),
350
237
  };
238
+ // @ts-expect-error 上游类型缺口:ClassMethodDecoratorContext 的形状比这里手工构造的上下文更严。
239
+ // 运行时有效(dsh-typert-protocol 的 addMarkerInitializer 只读 kind/name/static/private),见 verify 的 remote 小节。
351
240
  Remote(method)(Class.prototype[method], context);
352
241
  }
353
242
  }
@@ -357,6 +246,8 @@ class TTeamRemote extends TypertRemoteService {
357
246
 
358
247
  constructor(ctx) {
359
248
  super(ctx, "tTeam");
249
+ // @ts-expect-error 上游类型缺口:TypertRegistry.register(contribution) 未写进 TypertRegistryContract。
250
+ // 运行时有效:DSH 实际提供的 TypertRegistry 类实现了它(dsh-typert-registry/lib/index.js:398)。
360
251
  this.ctx.typert.register(TYPERT);
361
252
  }
362
253
 
@@ -371,6 +262,8 @@ class TTeamRemote extends TypertRemoteService {
371
262
  * @returns 带稳定 code 的 RemoteError。
372
263
  */
373
264
  hostUnavailable(name, hint) {
265
+ // @ts-expect-error 上游类型缺口:RemoteErrorDetailsMap 是**闭合**的 gateway 错误码集合,业务码不在其中。
266
+ // 运行时有效且是设计用法:plugin 自己的 tTeam/* 稳定码(映射文本是**远端**它时自行分类的,见 remote.js 顶部的业务码说明)。
374
267
  return new RemoteError("tTeam/host-unavailable", hint, { service: name });
375
268
  }
376
269
 
@@ -411,12 +304,14 @@ class TTeamRemote extends TypertRemoteService {
411
304
  // · 「其他窗口」在本仓根本不存在于宿主侧,纯属臆测的变体。
412
305
  // 因此无 code 的同类错误会落到下面的 settings-failed(可见且可诊断),而不是被文案猜成冲突。
413
306
  if (code === "SETTINGS_CONFLICT") {
307
+ // @ts-expect-error 上游类型缺口:同上,tTeam/* 业务码不在闭合的 RemoteErrorDetailsMap 里。
414
308
  throw new RemoteError("tTeam/conflict", message, { expectedRevision: String(expectedRevision) });
415
309
  }
416
310
  // 未知专家 slug 由 host 侧抛带稳定 code 的错误(lib/index.js 的 customError)。
417
311
  // **不再**用 message.includes("未知专家") 分类 —— 显示文案随 locale 变,用它做身份会让
418
312
  // 非中文 locale(或任何文案改写)下的错误直接退化成裸 Error,客户端只剩 gateway/internal(D-3)。
419
313
  if (code === UNKNOWN_SLUG_CODE) {
314
+ // @ts-expect-error 上游类型缺口:同上,tTeam/* 业务码不在闭合的 RemoteErrorDetailsMap 里。
420
315
  throw new RemoteError("tTeam/unknown-expert", message, {});
421
316
  }
422
317
  throw this.businessError(error, "tTeam/settings-failed");
@@ -445,6 +340,7 @@ class TTeamRemote extends TypertRemoteService {
445
340
  teams() {
446
341
  const service = this.ctx.get(TEAM_SERVICE);
447
342
  if (service === undefined) {
343
+ // @ts-expect-error 上游类型缺口:同上,tTeam/* 业务码不在闭合的 RemoteErrorDetailsMap 里。
448
344
  throw new RemoteError("tTeam/start-failed", "T专家 团队服务不可用(host 插件未加载)", { service: TEAM_SERVICE });
449
345
  }
450
346
  return service;
package/lib/skill.js CHANGED
@@ -1,3 +1,4 @@
1
+ // @ts-check
1
2
  /**
2
3
  * 随包发布的 skill:把「T专家 运维」与「DeepSeek Harness 项目知识」做成**随 npm 包分发**的 skill。
3
4
  *
@@ -74,8 +75,10 @@ export function frontmatterField(text, key) {
74
75
  *
75
76
  * 候选按序:`T_TEAM_REPO` 的父目录 → 数据目录的父目录 → 包目录的父目录。
76
77
  * 每个候选都要**真的存在 tz.sh** 才算数,否则返回空串(不猜)。
77
- * @param options - `dataDir` = 数据目录(默认 `~/.t-team`,本机是指向 `<ops>/data` 的软链);
78
- * `packageDir` = 本包根目录;`env` = 环境变量(测试可注入)。
78
+ * @param {object} [options] 调用选项
79
+ * @param {string} [options.dataDir] 数据目录(默认 `~/.t-team`,本机是指向 `<ops>/data` 的软链)
80
+ * @param {string} [options.packageDir] 本包根目录
81
+ * @param {Record<string, string | undefined>} [options.env] 环境变量(测试可注入)
79
82
  * @returns 运维台根目录,或空串。
80
83
  */
81
84
  export function resolveOpsRoot({ dataDir, packageDir, env = process.env } = {}) {