@fa-software/semi2-dsh 0.0.13 → 0.0.14-preview.20260922151653
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/README.md +20 -7
- package/lib/api/generated/documents.js +10 -1
- package/lib/api/generated/schema.js +2 -9
- package/lib/api/index.js +13 -0
- package/lib/client.js +54 -48
- package/lib/client.js.map +1 -1
- package/lib/contract.js +6 -0
- package/lib/domains/models/domain.js +11 -4
- package/lib/domains/models/web-search.js +61 -0
- package/lib/domains/redmine/domain.js +139 -0
- package/lib/domains/redmine/index.js +57 -0
- package/lib/domains/skills/candidates.js +4 -2
- package/lib/domains/skills/domain.js +5 -3
- package/lib/domains/workspace/domain.js +3 -2
- package/lib/hub/facts.js +1 -0
- package/lib/index.js +10 -11
- package/lib/service/audit.js +21 -7
- package/lib/service/config-service.js +9 -22
- package/lib/service/lifecycle.js +32 -34
- package/lib/service/namespaces.js +46 -16
- package/lib/service/ports.js +36 -13
- package/lib/service/publish.js +8 -8
- package/lib/service/registry.js +2 -0
- package/lib/service/sessions.js +90 -0
- package/lib/service/settings-sections.js +23 -62
- package/lib/types/api/generated/documents.d.ts +1 -1
- package/lib/types/api/generated/graphql.d.ts +27 -5
- package/lib/types/api/generated/schema.d.ts +265 -60
- package/lib/types/api/index.d.ts +13 -1
- package/lib/types/contract.d.ts +11 -0
- package/lib/types/domains/models/web-search.d.ts +8 -0
- package/lib/types/domains/redmine/domain.d.ts +59 -0
- package/lib/types/domains/redmine/index.d.ts +10 -0
- package/lib/types/domains/skills/domain.d.ts +1 -1
- package/lib/types/domains/types.d.ts +8 -2
- package/lib/types/hub/facts.d.ts +18 -0
- package/lib/types/index.d.ts +28 -24
- package/lib/types/service/audit.d.ts +10 -0
- package/lib/types/service/config-service.d.ts +3 -1
- package/lib/types/service/lifecycle.d.ts +5 -0
- package/lib/types/service/namespaces.d.ts +15 -17
- package/lib/types/service/ports.d.ts +19 -15
- package/lib/types/service/publish.d.ts +6 -1
- package/lib/types/service/sessions.d.ts +8 -0
- package/lib/types/service/settings-sections.d.ts +5 -14
- package/lib/types/web/routes.d.ts +4 -2
- package/lib/types/web/welcome-ack.d.ts +3 -3
- package/lib/web/routes.js +25 -1
- package/lib/web/welcome-ack.js +5 -5
- package/package.json +19 -11
package/lib/contract.js
CHANGED
|
@@ -10,6 +10,12 @@ export const SEMI2_SECTION_ORDERS = {
|
|
|
10
10
|
npcContract: 2,
|
|
11
11
|
cnbWorkstation: 3,
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* 插件自身设置段的条目 id:dsh 的设置命名空间就是 Profile 插件条目 id(bundle patch 里那一行的 id),
|
|
15
|
+
* 状态 / 健康 / 归属表 / 工作区映射都写在这一段里。host 与浏览器半边共用这一个常量
|
|
16
|
+
* (`cordis.patch.yml` 的插件行 id 必须与它一致,`test/dsh/unit/profile-patch.test.mjs` 对账)。
|
|
17
|
+
*/
|
|
18
|
+
export const SEMI2_SETTINGS_ENTRY = 'semi2-dsh';
|
|
13
19
|
/** 官方人设前缀段名(号段基准);服务不认这个名字时按 0 兜底。 */
|
|
14
20
|
const PERSONA_PREFIX_ORDER_NAME = 'DEPLOYMENT_PERSONA_PREFIX';
|
|
15
21
|
/** 段的绝对位置 = 官方 persona 段号 + 本表偏移(服务缺席 / 不认名字时以偏移值兜底)。 */
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { compileBoot, connectionRoute, providerProfile, remapSelections, } from '../../hub/compile.js';
|
|
2
2
|
import { sectionValue } from '../../service/namespaces.js';
|
|
3
|
+
import { reclaimWebSearchKeyEnv, syncWebSearchKeyEnv } from './web-search.js';
|
|
3
4
|
/** 自铸路由的键形状(semi2-<32 hex>):归属表里没有记录时按名字兜底认领。 */
|
|
4
5
|
const LEGACY_OWNED_PROVIDER = /^semi2-[\da-f]{32}$/;
|
|
6
|
+
/** 本插件自铸的密钥引用名(`connectionApiKeyEnv` 的形状):账本丢了也能按它认领注入过的路由。 */
|
|
7
|
+
const OWNED_KEY_ENV = /^SEMI2_MODEL_KEY_[\dA-F]+$/;
|
|
5
8
|
/** 本插件在 `llm-deepseek` 段写过的键:撤销只删这几个,不碰同段里用户自己加的键。 */
|
|
6
9
|
export const DEEPSEEK_OWNED_KEYS = ['apiKeyEnv', 'baseURL', 'models'];
|
|
7
10
|
/** 从既有 provider profile 上取密钥引用名(回收归属表外的自铸路由时连带清理其密钥)。 */
|
|
@@ -44,6 +47,7 @@ export class ModelsDomain {
|
|
|
44
47
|
});
|
|
45
48
|
else if (run.owned.deepseek)
|
|
46
49
|
await this.host.ports.clearKeys(run.settings, 'llm-deepseek', DEEPSEEK_OWNED_KEYS);
|
|
50
|
+
await syncWebSearchKeyEnv(this.host, run, remapped.deepseek);
|
|
47
51
|
run.compiled = remapped;
|
|
48
52
|
run.owned = {
|
|
49
53
|
...run.owned,
|
|
@@ -59,6 +63,7 @@ export class ModelsDomain {
|
|
|
59
63
|
const { staleCredentials } = await this.applyProviders(run.settings, {}, run.owned);
|
|
60
64
|
if (run.owned.deepseek)
|
|
61
65
|
await this.host.ports.clearKeys(run.settings, 'llm-deepseek', DEEPSEEK_OWNED_KEYS);
|
|
66
|
+
await reclaimWebSearchKeyEnv(this.host, run);
|
|
62
67
|
run.staleCredentials = staleCredentials;
|
|
63
68
|
this.compiled = null;
|
|
64
69
|
}
|
|
@@ -102,15 +107,17 @@ export class ModelsDomain {
|
|
|
102
107
|
injected[fallback] = providerProfile({ ...provider, preset: false });
|
|
103
108
|
routes.set(route, fallback);
|
|
104
109
|
}
|
|
105
|
-
//
|
|
106
|
-
//
|
|
110
|
+
// 归属表外的自有路由一并回收,避免留下死路由(重载与登出都走这里):名字是自铸形(semi2-<hex>),
|
|
111
|
+
// 或 profile 上的密钥引用是本插件自铸的(账本丢了也能认领)。密钥引用只能从这里取——账本里没有这条记录。
|
|
107
112
|
const staleCredentials = [];
|
|
108
113
|
const kept = Object.fromEntries(Object.entries(existing).filter(([id, profile]) => {
|
|
109
114
|
if (owned.has(id))
|
|
110
115
|
return false;
|
|
111
|
-
if (!LEGACY_OWNED_PROVIDER.test(id))
|
|
112
|
-
return true;
|
|
113
116
|
const env = apiKeyEnvOf(profile);
|
|
117
|
+
const ours = LEGACY_OWNED_PROVIDER.test(id) ||
|
|
118
|
+
(env !== undefined && OWNED_KEY_ENV.test(env));
|
|
119
|
+
if (!ours)
|
|
120
|
+
return true;
|
|
114
121
|
if (env !== undefined)
|
|
115
122
|
staleCredentials.push(env);
|
|
116
123
|
return false;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { sectionValue } from '../../service/namespaces.js';
|
|
2
|
+
const WEB_SEARCH_DEEPSEEK_NAMESPACE = 'web-search-deepseek';
|
|
3
|
+
const WEB_SEARCH_API_KEY_ENV_KEY = 'apiKeyEnv';
|
|
4
|
+
const DEFAULT_DEEPSEEK_SEARCH_KEY_ENV = 'DEEPSEEK_API_KEY';
|
|
5
|
+
/**
|
|
6
|
+
* 官方搜索默认解析 `DEEPSEEK_API_KEY`,而 hub 下发的是连接级引用;这里只对齐引用,
|
|
7
|
+
* 搜索端点与其他搜索配置仍归本地配置的所有者。
|
|
8
|
+
*/
|
|
9
|
+
export async function syncWebSearchKeyEnv(host, run, deepseek) {
|
|
10
|
+
const previous = run.previousOwned.deepseekSearchKeyEnv;
|
|
11
|
+
const current = webSearchKeyEnvOf(run.settings);
|
|
12
|
+
try {
|
|
13
|
+
if (deepseek !== null) {
|
|
14
|
+
const userOwned = current !== undefined &&
|
|
15
|
+
current !== previous &&
|
|
16
|
+
current !== DEFAULT_DEEPSEEK_SEARCH_KEY_ENV;
|
|
17
|
+
if (userOwned) {
|
|
18
|
+
run.owned.deepseekSearchKeyEnv = null;
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (current !== deepseek.apiKeyEnv) {
|
|
22
|
+
await host.ports.updateNamespace(run.settings, WEB_SEARCH_DEEPSEEK_NAMESPACE, { [WEB_SEARCH_API_KEY_ENV_KEY]: deepseek.apiKeyEnv });
|
|
23
|
+
}
|
|
24
|
+
run.owned.deepseekSearchKeyEnv = deepseek.apiKeyEnv;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (previous === null)
|
|
28
|
+
return;
|
|
29
|
+
if (current !== undefined && current !== previous) {
|
|
30
|
+
run.owned.deepseekSearchKeyEnv = null;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (current !== undefined) {
|
|
34
|
+
await clearWebSearchKeyEnv(host, run.settings);
|
|
35
|
+
}
|
|
36
|
+
run.owned.deepseekSearchKeyEnv = null;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
host.warn('DeepSeek 网页搜索密钥引用同步失败,搜索按当前本地配置继续');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function reclaimWebSearchKeyEnv(host, run) {
|
|
43
|
+
const owned = run.owned.deepseekSearchKeyEnv;
|
|
44
|
+
if (owned === null)
|
|
45
|
+
return;
|
|
46
|
+
try {
|
|
47
|
+
if (webSearchKeyEnvOf(run.settings) === owned)
|
|
48
|
+
await clearWebSearchKeyEnv(host, run.settings);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
host.warn('DeepSeek 网页搜索密钥引用回收失败');
|
|
52
|
+
}
|
|
53
|
+
run.owned.deepseekSearchKeyEnv = null;
|
|
54
|
+
}
|
|
55
|
+
async function clearWebSearchKeyEnv(host, settings) {
|
|
56
|
+
await host.ports.clearKeys(settings, WEB_SEARCH_DEEPSEEK_NAMESPACE, [WEB_SEARCH_API_KEY_ENV_KEY]);
|
|
57
|
+
}
|
|
58
|
+
function webSearchKeyEnvOf(settings) {
|
|
59
|
+
const value = sectionValue(settings, WEB_SEARCH_DEEPSEEK_NAMESPACE).apiKeyEnv;
|
|
60
|
+
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
61
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/** MCP 服务的命名空间(工具名 = `mcp__redmine__<tool>`)。 */
|
|
2
|
+
export const REDMINE_SERVER_NAME = 'redmine';
|
|
3
|
+
/** 单次工具调用上限:Redmine 是普通 REST 调用,附件下载也在同一档。 */
|
|
4
|
+
const TOOL_CALL_TIMEOUT_MS = 60_000;
|
|
5
|
+
/** boot 事实 → 挂载配置;密钥只进子进程 env,不落盘、不进日志。 */
|
|
6
|
+
export function redmineMcpConfig(baseUrl, apiKey, faces) {
|
|
7
|
+
return {
|
|
8
|
+
transport: 'stdio',
|
|
9
|
+
serverName: REDMINE_SERVER_NAME,
|
|
10
|
+
command: faces.command(),
|
|
11
|
+
args: [],
|
|
12
|
+
env: {
|
|
13
|
+
REDMINE_URL: baseUrl,
|
|
14
|
+
REDMINE_API_KEY: apiKey,
|
|
15
|
+
// 只读靠上游开关关掉写工具(本插件不另设工具白名单)。
|
|
16
|
+
REDMINE_MCP_READ_ONLY: 'true',
|
|
17
|
+
},
|
|
18
|
+
cwd: faces.cwd(),
|
|
19
|
+
toolCallTimeoutMs: TOOL_CALL_TIMEOUT_MS,
|
|
20
|
+
failOnStartupError: false,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
export class RedmineDomain {
|
|
24
|
+
host;
|
|
25
|
+
faces;
|
|
26
|
+
id = 'redmine';
|
|
27
|
+
mounted = null;
|
|
28
|
+
degraded = false;
|
|
29
|
+
pending = false;
|
|
30
|
+
constructor(host, faces) {
|
|
31
|
+
this.host = host;
|
|
32
|
+
this.faces = faces;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* boot 事实落地:
|
|
36
|
+
* - 块缺失 / null(读取失败)→ 只 warn,保持现状(不动已挂的服务);
|
|
37
|
+
* - 两项齐备 → 配置没变就不重挂,变了先卸再挂;
|
|
38
|
+
* - 任一项 null(权威未配置)→ 卸载。
|
|
39
|
+
*/
|
|
40
|
+
async applyBoot(run) {
|
|
41
|
+
const fact = run.boot.runtimeRedmine;
|
|
42
|
+
if (fact === null || fact === undefined) {
|
|
43
|
+
this.host.warn('Redmine 配置读取失败(runtimeRedmine),保持现状不改动已挂服务');
|
|
44
|
+
this.degraded = true;
|
|
45
|
+
this.host.patchState({ redmine: null });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const current = normalize(fact);
|
|
49
|
+
this.host.patchState({ redmine: current });
|
|
50
|
+
if (current.baseUrl === null || current.apiKey === null) {
|
|
51
|
+
await this.unmount();
|
|
52
|
+
this.degraded = false;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const config = redmineMcpConfig(current.baseUrl, current.apiKey, this.faces);
|
|
56
|
+
if (this.mounted !== null && sameConfig(this.mounted.config, config)) {
|
|
57
|
+
this.degraded = false;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
await this.unmount();
|
|
61
|
+
try {
|
|
62
|
+
const server = await this.faces.mount(config);
|
|
63
|
+
this.mounted = { config, server };
|
|
64
|
+
this.degraded = false;
|
|
65
|
+
this.pending = !(await server.ready);
|
|
66
|
+
if (this.pending)
|
|
67
|
+
this.host.warn('Redmine MCP 服务尚未就绪(工具服务缺席或连接慢),就绪后工具自动出现');
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
this.mounted = null;
|
|
71
|
+
this.degraded = true;
|
|
72
|
+
this.pending = false;
|
|
73
|
+
this.host.warn(`Redmine MCP 服务挂载失败(${error instanceof Error ? error.message : String(error)}),本次不提供 Redmine 工具`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** 登出 / 服务端整体撤下:Redmine 工具随身份一起收走。 */
|
|
77
|
+
async revert(_run) {
|
|
78
|
+
await this.unmount();
|
|
79
|
+
this.pending = false;
|
|
80
|
+
this.host.patchState({ redmine: null });
|
|
81
|
+
}
|
|
82
|
+
async unmount() {
|
|
83
|
+
const current = this.mounted;
|
|
84
|
+
this.mounted = null;
|
|
85
|
+
if (current === null)
|
|
86
|
+
return;
|
|
87
|
+
try {
|
|
88
|
+
await current.server.dispose();
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
this.host.warn(`Redmine MCP 服务卸载失败(${error instanceof Error ? error.message : String(error)})`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
statusFields(state) {
|
|
95
|
+
return { redmine: renderStatus(state) };
|
|
96
|
+
}
|
|
97
|
+
summary(state) {
|
|
98
|
+
return state.redmine === null ? null : renderStatus(state);
|
|
99
|
+
}
|
|
100
|
+
health() {
|
|
101
|
+
const state = this.host.state();
|
|
102
|
+
// 未连接(本机没凭据)不是降级:本轮根本没有服务端事实可读。
|
|
103
|
+
if (state.status === 'local')
|
|
104
|
+
return { state: 'ok', note: '未连接' };
|
|
105
|
+
if (state.redmine === null || this.degraded)
|
|
106
|
+
return { state: 'degraded', note: 'Redmine 配置读取 / 挂载失败,本次不提供工具' };
|
|
107
|
+
if (state.redmine.baseUrl === null || state.redmine.apiKey === null)
|
|
108
|
+
return { state: 'ok', note: '未配置' };
|
|
109
|
+
if (this.pending)
|
|
110
|
+
return { state: 'degraded', note: 'MCP 服务尚未就绪,工具稍后出现' };
|
|
111
|
+
return { state: 'ok', note: `${REDMINE_SERVER_NAME} 已挂载` };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** 空串与 null 同义(服务端只发 null,这里是防御):两项齐备才算可用。 */
|
|
115
|
+
function normalize(fact) {
|
|
116
|
+
return {
|
|
117
|
+
baseUrl: fact.baseUrl === null || fact.baseUrl === '' ? null : fact.baseUrl,
|
|
118
|
+
apiKey: fact.apiKey === null || fact.apiKey === '' ? null : fact.apiKey,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function sameConfig(a, b) {
|
|
122
|
+
return a.command === b.command &&
|
|
123
|
+
a.cwd === b.cwd &&
|
|
124
|
+
a.env.REDMINE_URL === b.env.REDMINE_URL &&
|
|
125
|
+
a.env.REDMINE_API_KEY === b.env.REDMINE_API_KEY;
|
|
126
|
+
}
|
|
127
|
+
/** 状态行文案:未连接 / 读取失败 / 未配置 / 已启用 + 地址。 */
|
|
128
|
+
function renderStatus(state) {
|
|
129
|
+
if (state.status === 'local')
|
|
130
|
+
return '';
|
|
131
|
+
const fact = state.redmine;
|
|
132
|
+
if (fact === null)
|
|
133
|
+
return '读取失败';
|
|
134
|
+
if (fact.baseUrl === null)
|
|
135
|
+
return '未配置(缺 Redmine 地址)';
|
|
136
|
+
if (fact.apiKey === null)
|
|
137
|
+
return '未配置(本人未填密钥)';
|
|
138
|
+
return `已启用 · ${fact.baseUrl}`;
|
|
139
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { RedmineDomain, } from './domain.js';
|
|
2
|
+
/** 等领域服务的等待上限:超时只记降级(光纤留着,工具服务晚到时官方那边自己接上)。 */
|
|
3
|
+
const MOUNT_WAIT_MS = 2_000;
|
|
4
|
+
/** 起进程的命令:镜像里在 PATH 上;npm 消费者可用 `SEMI2_REDMINE_MCP_BIN` 指到别处(同 CodeGraph 口径)。 */
|
|
5
|
+
export function redmineMcpCommand(env) {
|
|
6
|
+
const override = env.SEMI2_REDMINE_MCP_BIN?.trim();
|
|
7
|
+
return override === undefined || override === '' ? 'redmine-mcp-server' : override;
|
|
8
|
+
}
|
|
9
|
+
/** 子进程工作目录 = 本面工作目录(同 CodeGraph 挂载行)。 */
|
|
10
|
+
export function redmineMcpCwd(env) {
|
|
11
|
+
const workspace = env.SEMI2_WORKSTATION_CWD?.trim();
|
|
12
|
+
return workspace === undefined || workspace === '' ? process.cwd() : workspace;
|
|
13
|
+
}
|
|
14
|
+
/** 到时限还没就绪即 false;激活失败也算 false(调用方只记一条 warn)。 */
|
|
15
|
+
async function within(promise, ms) {
|
|
16
|
+
let timer;
|
|
17
|
+
try {
|
|
18
|
+
return await Promise.race([
|
|
19
|
+
Promise.resolve(promise).then(() => true, () => false),
|
|
20
|
+
new Promise((resolve) => {
|
|
21
|
+
timer = setTimeout(() => {
|
|
22
|
+
resolve(false);
|
|
23
|
+
}, ms);
|
|
24
|
+
}),
|
|
25
|
+
]);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
if (timer !== undefined)
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** 运行面能力:动态 import 官方包 + 在本领域的 ctx 上挂一根光纤(dispose 由领域持有)。 */
|
|
33
|
+
export function createRedmineFaces(ctx) {
|
|
34
|
+
return {
|
|
35
|
+
mount: async (config) => {
|
|
36
|
+
const module = (await import('@deepseek-ai/dsh-mcp-client'));
|
|
37
|
+
const fiber = ctx.plugin(module, config);
|
|
38
|
+
return {
|
|
39
|
+
ready: within(fiber, MOUNT_WAIT_MS),
|
|
40
|
+
dispose: async () => {
|
|
41
|
+
await fiber.dispose();
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
command: () => redmineMcpCommand(process.env),
|
|
46
|
+
cwd: () => redmineMcpCwd(process.env),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export const redmineDomain = {
|
|
50
|
+
id: 'redmine',
|
|
51
|
+
operations: ['RuntimeBoot'],
|
|
52
|
+
statusKeys: ['redmine'],
|
|
53
|
+
apply(ctx, config) {
|
|
54
|
+
const domain = new RedmineDomain(config.host, createRedmineFaces(ctx));
|
|
55
|
+
config.host.attach(domain);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -16,11 +16,12 @@ export function candidateOf(skill, warn) {
|
|
|
16
16
|
description: skill.description,
|
|
17
17
|
// exactOptionalPropertyTypes:可选字段只能用条件展开,不能赋 undefined。
|
|
18
18
|
...(skill.whenToUse ? { whenToUse: skill.whenToUse } : {}),
|
|
19
|
+
...(skill.metadata === null ? {} : { metadata: skill.metadata }),
|
|
19
20
|
rank: HUB_SKILL_RANK,
|
|
20
21
|
locator: skill.id,
|
|
21
22
|
source: 'custom',
|
|
22
23
|
provider: 'semi2-hub',
|
|
23
|
-
invocation: {
|
|
24
|
+
invocation: { ...skill.invocation },
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
export function definitionOf(skill) {
|
|
@@ -28,9 +29,10 @@ export function definitionOf(skill) {
|
|
|
28
29
|
name: skill.name,
|
|
29
30
|
description: skill.description,
|
|
30
31
|
...(skill.whenToUse ? { whenToUse: skill.whenToUse } : {}),
|
|
32
|
+
...(skill.metadata === null ? {} : { metadata: skill.metadata }),
|
|
31
33
|
content: skill.content,
|
|
32
34
|
source: 'custom',
|
|
33
35
|
provider: 'semi2-hub',
|
|
34
|
-
invocation: {
|
|
36
|
+
invocation: { ...skill.invocation },
|
|
35
37
|
};
|
|
36
38
|
}
|
|
@@ -30,14 +30,14 @@ export class SkillsDomain {
|
|
|
30
30
|
return this.current;
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* 控制台技能目录:`
|
|
33
|
+
* 控制台技能目录:`runtimeSkillCatalog` 为 null/undefined 都算「没拿到权威目录」——null 只降级该块,
|
|
34
34
|
* null ≠ 空数组(provider 据此报 complete:false,dsh 才不把抖动缓存成空目录)。
|
|
35
35
|
*/
|
|
36
36
|
applyBoot(run) {
|
|
37
37
|
const boot = run.boot;
|
|
38
|
-
const skills = boot.
|
|
38
|
+
const skills = boot.runtimeSkillCatalog;
|
|
39
39
|
if (skills === null || skills === undefined) {
|
|
40
|
-
this.host.warn('技能目录读取失败(
|
|
40
|
+
this.host.warn('技能目录读取失败(runtimeSkillCatalog),本次不提供控制台技能');
|
|
41
41
|
this.degraded = true;
|
|
42
42
|
this.current = null;
|
|
43
43
|
return Promise.resolve();
|
|
@@ -49,6 +49,8 @@ export class SkillsDomain {
|
|
|
49
49
|
name: skill.name,
|
|
50
50
|
description: skill.description,
|
|
51
51
|
whenToUse: skill.whenToUse,
|
|
52
|
+
invocation: { ...skill.invocation },
|
|
53
|
+
metadata: skill.metadata === null ? null : { ...skill.metadata },
|
|
52
54
|
content: skill.content,
|
|
53
55
|
version: skill.version,
|
|
54
56
|
files: fileFactsOf(skill),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { persistedWorkspaces, trackedWorkspaces, WorkspaceBook, } from '../../hub/workspace-book.js';
|
|
2
2
|
import { renderWorkspaces } from '../../hub/status.js';
|
|
3
3
|
import { codeOf } from '../../hub/errors.js';
|
|
4
|
-
import {
|
|
4
|
+
import { SEMI2_SETTINGS_ENTRY } from '../../service/namespaces.js';
|
|
5
5
|
import { projectForCwd, syncAll, syncBySlug, syncOne, } from './resolve.js';
|
|
6
6
|
export class WorkspaceDomain {
|
|
7
7
|
host;
|
|
@@ -125,7 +125,8 @@ export class WorkspaceDomain {
|
|
|
125
125
|
if (settings === undefined)
|
|
126
126
|
return;
|
|
127
127
|
try {
|
|
128
|
-
|
|
128
|
+
// 合并写入:同一段里还有状态与归属表,整体替换会把它们一起重置。
|
|
129
|
+
await this.host.ports.updateNamespace(settings, SEMI2_SETTINGS_ENTRY, {
|
|
129
130
|
entries: persistedWorkspaces(this.tracked()),
|
|
130
131
|
});
|
|
131
132
|
}
|
package/lib/hub/facts.js
CHANGED
package/lib/index.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import z from '@deepseek-ai/schemastery';
|
|
2
2
|
import { installConfiguration } from './service/config-service.js';
|
|
3
|
+
import { STATE_CONFIG_FIELDS } from './service/settings-sections.js';
|
|
3
4
|
import { installWebSurface } from './web/index.js';
|
|
4
|
-
export { SEMI2_RUNTIME_SERVICE, SEMI2_SECTION_ORDERS, runtimeFromCnbEnv, runtimeFromContainerEnv, semi2SectionOrder, } from './contract.js';
|
|
5
|
+
export { SEMI2_RUNTIME_SERVICE, SEMI2_SECTION_ORDERS, SEMI2_SETTINGS_ENTRY, runtimeFromCnbEnv, runtimeFromContainerEnv, semi2SectionOrder, } from './contract.js';
|
|
5
6
|
// 事件签名声明(semi2/config-updated / semi2/workspaces-updated)唯一出处。
|
|
6
7
|
import './events.js';
|
|
7
8
|
export const name = 'semi2-dsh';
|
|
8
9
|
/**
|
|
9
10
|
* 可调参数:默认值 = 各模块曾经的常量;bundle patch 行不写 config,消费方 overlay 才覆盖。
|
|
10
11
|
* 「config 明确值 → 环境变量 → 内置常量」的取值顺序在 apply 里一次归一。
|
|
12
|
+
*
|
|
13
|
+
* 状态 / 健康 / 工作区 tab / 归属表 / 工作区映射(`STATE_CONFIG_FIELDS`)同在这一段里,但全部声明为
|
|
14
|
+
* volatile(可实时写、不重挂载):dsh 的 `describe()` 只认有 volatile 字段的条目,本插件的设置段
|
|
15
|
+
* 靠它才存在。这些字段一律经 `describe()` 读写,不在 `apply` 里当普通值取(volatile 字段在 config 上是引用)。
|
|
11
16
|
*/
|
|
12
17
|
export const Config = z.object({
|
|
13
18
|
/** 等运行面契约(`semi2Runtime`)出现的上限:超时按宿主直跑处理。 */
|
|
@@ -16,15 +21,16 @@ export const Config = z.object({
|
|
|
16
21
|
domainAttachTimeoutMs: z.number().default(2_000),
|
|
17
22
|
/** 等 settings 服务的上限:本面无该服务时不阻塞启动。 */
|
|
18
23
|
settingsTimeoutMs: z.number().default(5_000),
|
|
19
|
-
/**
|
|
24
|
+
/** 等设置段可读可写(条目 ACTIVE 且声明了 volatile 字段)的上限。 */
|
|
20
25
|
namespaceWaitMs: z.number().default(2_000),
|
|
21
26
|
/** 凭据存储首次读盘的重试次数与间隔。 */
|
|
22
27
|
credentialRetryTries: z.number().default(10),
|
|
23
28
|
credentialRetryIntervalMs: z.number().default(150),
|
|
24
29
|
/** webServer 在而校验面迟迟不出现时,多久补一条诊断日志。 */
|
|
25
30
|
webFenceTimeoutMs: z.number().default(5_000),
|
|
26
|
-
/** 登录 / 登出 / 健康路由的请求体上限(字节)。 */
|
|
31
|
+
/** 登录 / 登出 / 同步 / 健康路由的请求体上限(字节)。 */
|
|
27
32
|
webRouteBodyLimitBytes: z.number().default(8_192),
|
|
33
|
+
...STATE_CONFIG_FIELDS,
|
|
28
34
|
});
|
|
29
35
|
/** CodeGraph 开关:挂载行的 `disabled` 同口径(bundle patch),插件只把它反映到健康行。 */
|
|
30
36
|
function codegraphDisabled(env) {
|
|
@@ -58,14 +64,6 @@ export function apply(ctx, config) {
|
|
|
58
64
|
ctx.provide('semi2Health', configuration.health);
|
|
59
65
|
// 运行面(NPC 面)回报生命周期与交付事实的出口;上报失败只 warn。
|
|
60
66
|
ctx.provide('semi2Audit', configuration.audit);
|
|
61
|
-
// 优雅卸载 = 本面会话结束:进程被强杀(SIGKILL / 容器停止)没有结束行,属已知边界。
|
|
62
|
-
ctx.effect(() => () => {
|
|
63
|
-
void configuration.audit.report({
|
|
64
|
-
action: 'SESSION_END',
|
|
65
|
-
outcome: 'SUCCESS',
|
|
66
|
-
detail: { platform: configuration.config.platform, reason: 'dispose' },
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
67
|
// 索引由使用方建立、工具由官方 MCP 服务提供:插件侧没有可失败的后台动作,只标注开关状态。
|
|
70
68
|
configuration.registerHealth({
|
|
71
69
|
key: 'healthCodegraph',
|
|
@@ -76,6 +74,7 @@ export function apply(ctx, config) {
|
|
|
76
74
|
installWebSurface(ctx, {
|
|
77
75
|
login: input => configuration.session.login(input),
|
|
78
76
|
logout: () => configuration.session.logout(),
|
|
77
|
+
sync: scope => configuration.session.sync(scope),
|
|
79
78
|
health: () => configuration.health.snapshot(),
|
|
80
79
|
}, (contributor) => {
|
|
81
80
|
configuration.registerHealth(contributor);
|
package/lib/service/audit.js
CHANGED
|
@@ -1,19 +1,33 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 上报一条运行面事件并回结果:本面标识(`face`)与运行面契约里的任务引用在这里补齐,
|
|
3
|
+
* 调用方(会话订阅 / 适配器)不必各自知道这两个事实从哪来。
|
|
4
|
+
*/
|
|
5
|
+
export async function reportEventResult(engine, event) {
|
|
6
|
+
if (engine.connection() === null)
|
|
7
|
+
return { ok: false, code: 'MISSING_ACCESS_TOKEN' };
|
|
8
|
+
// 任务引用来自运行面契约(NPC 适配器解析触发后填),本插件不自己解析触发事实。
|
|
9
|
+
const taskRef = engine.contract()?.taskRef;
|
|
3
10
|
try {
|
|
4
|
-
if (engine.connection() === null)
|
|
5
|
-
return;
|
|
6
11
|
const outcome = await engine.api.reportEvent({
|
|
7
12
|
...event,
|
|
13
|
+
...(taskRef === undefined || taskRef === '' ? {} : { runRef: taskRef }),
|
|
8
14
|
detail: { face: faceOf(engine), ...(event.detail ?? {}) },
|
|
9
15
|
});
|
|
10
|
-
|
|
11
|
-
engine.warn(`运行面事件上报失败(${event.action}):${outcome.errorCode ?? '未知错误'}`);
|
|
16
|
+
return { ok: outcome.data !== null, code: outcome.errorCode };
|
|
12
17
|
}
|
|
13
18
|
catch (error) {
|
|
14
|
-
|
|
19
|
+
return {
|
|
20
|
+
ok: false,
|
|
21
|
+
code: error instanceof Error ? error.message : String(error),
|
|
22
|
+
};
|
|
15
23
|
}
|
|
16
24
|
}
|
|
25
|
+
/** 生命周期事件:动作名与结果名都用 hub SDL 的枚举名,两侧不各维护一张映射表。 */
|
|
26
|
+
export async function reportEvent(engine, event) {
|
|
27
|
+
const result = await reportEventResult(engine, event);
|
|
28
|
+
if (!result.ok)
|
|
29
|
+
engine.warn(`运行面事件上报失败(${event.action}):${result.code ?? '未知错误'}`);
|
|
30
|
+
}
|
|
17
31
|
/** 本面标识:契约里有就用它;宿主直跑没有契约,记 host。 */
|
|
18
32
|
export function faceOf(engine) {
|
|
19
33
|
const declared = engine.contract()?.face;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createSemi2Api } from '../api/index.js';
|
|
2
2
|
import { createPorts } from './ports.js';
|
|
3
3
|
import { Orchestrator } from './orchestrator.js';
|
|
4
|
-
import {
|
|
4
|
+
import { reportEventResult } from './audit.js';
|
|
5
|
+
import { installSessionReporting } from './sessions.js';
|
|
5
6
|
import * as lifecycle from './lifecycle.js';
|
|
6
7
|
/**
|
|
7
8
|
* 装配三个能力面并开始后台领取:领域挂载与首次取数都在后台——不阻塞插件树 settle,
|
|
@@ -23,9 +24,13 @@ export function installConfiguration(ctx, options = {}) {
|
|
|
23
24
|
createSemi2Api({
|
|
24
25
|
readConnection: () => engineRef.current?.connection() ?? null,
|
|
25
26
|
environment: { ...process.env },
|
|
26
|
-
|
|
27
|
+
// 契约缺席即宿主直跑:本面标识回落 `host`,别让会话落成「未标注」。
|
|
28
|
+
readFace: () => engineRef.current?.contract()?.face ??
|
|
29
|
+
(engineRef.current?.platform === 'host' ? 'host' : undefined),
|
|
27
30
|
}), ports, { ...process.env }, options);
|
|
28
31
|
engineRef.current = orchestrator;
|
|
32
|
+
// 客户端会话日志的事实来源:旁听 dsh 的会话创建 / 销毁(服务缺席时自动跳过)。
|
|
33
|
+
installSessionReporting(orchestrator);
|
|
29
34
|
void orchestrator
|
|
30
35
|
.mountDomains()
|
|
31
36
|
.then(() => lifecycle.start(orchestrator))
|
|
@@ -37,29 +42,11 @@ export function installConfiguration(ctx, options = {}) {
|
|
|
37
42
|
session: {
|
|
38
43
|
login: input => lifecycle.login(orchestrator, input),
|
|
39
44
|
logout: () => lifecycle.logout(orchestrator),
|
|
45
|
+
sync: scope => lifecycle.syncByScope(orchestrator, scope),
|
|
40
46
|
},
|
|
41
47
|
health: { snapshot: () => orchestrator.healthSnapshot() },
|
|
42
48
|
audit: {
|
|
43
|
-
report:
|
|
44
|
-
if (orchestrator.connection() === null)
|
|
45
|
-
return { ok: false, code: 'MISSING_ACCESS_TOKEN' };
|
|
46
|
-
try {
|
|
47
|
-
const outcome = await orchestrator.api.reportEvent({
|
|
48
|
-
...event,
|
|
49
|
-
detail: {
|
|
50
|
-
face: faceOf(orchestrator),
|
|
51
|
-
...(event.detail ?? {}),
|
|
52
|
-
},
|
|
53
|
-
});
|
|
54
|
-
return { ok: outcome.data !== null, code: outcome.errorCode };
|
|
55
|
-
}
|
|
56
|
-
catch (error) {
|
|
57
|
-
return {
|
|
58
|
-
ok: false,
|
|
59
|
-
code: error instanceof Error ? error.message : String(error),
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
},
|
|
49
|
+
report: event => reportEventResult(orchestrator, event),
|
|
63
50
|
},
|
|
64
51
|
registerHealth: (contributor) => {
|
|
65
52
|
orchestrator.registerHealthContributor(contributor);
|