@fa-software/semi2-dsh 0.0.16 → 0.0.17-preview.20260929054409

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.
@@ -1,11 +1,11 @@
1
1
  import { codeOf } from "../../hub/errors.js";
2
- import { hasSection, sectionValue, userSection } from "../../service/namespaces.js";
2
+ import { hasSection } from "../../service/namespaces.js";
3
3
  const WEB_SEARCH_DEEPSEEK_NAMESPACE = 'web-search-deepseek';
4
4
  const WEB_SEARCH_API_KEY_ENV_KEY = 'apiKeyEnv';
5
5
  const DEFAULT_DEEPSEEK_SEARCH_KEY_ENV = 'DEEPSEEK_API_KEY';
6
6
  /**
7
7
  * 官方搜索默认解析 `DEEPSEEK_API_KEY`,而 hub 下发的是连接级引用;这里只对齐引用,
8
- * 搜索端点与其他搜索配置仍归本地配置的所有者。
8
+ * 搜索端点与其他搜索配置仍归本地配置的所有者。接管判定用写瞬间的生效值(官方事务内)。
9
9
  * @returns 本进程写进该段的引用;null = 没写或已交还用户。
10
10
  */
11
11
  export async function syncWebSearchKeyEnv(host, run, deepseek, previous) {
@@ -15,25 +15,29 @@ export async function syncWebSearchKeyEnv(host, run, deepseek, previous) {
15
15
  host.warn('DeepSeek 网页搜索密钥引用同步失败,搜索按当前本地配置继续');
16
16
  return previous;
17
17
  }
18
- const current = webSearchKeyEnvOf(run.settings);
19
- // 用户自写的引用(或组合层默认之外的既有值)不接管:只有我们写过的前值才覆盖。
20
- if (current !== undefined &&
21
- current !== previous &&
22
- current !== DEFAULT_DEEPSEEK_SEARCH_KEY_ENV)
23
- return null;
24
18
  try {
25
- await host.ports.replaceNamespace(run.settings, WEB_SEARCH_DEEPSEEK_NAMESPACE, {
26
- ...userSection(run.settings, WEB_SEARCH_DEEPSEEK_NAMESPACE),
27
- [WEB_SEARCH_API_KEY_ENV_KEY]: deepseek.apiKeyEnv,
19
+ return await host.ports.replaceNamespace(run.settings, WEB_SEARCH_DEEPSEEK_NAMESPACE, (view) => {
20
+ const current = keyEnvOf(view);
21
+ // 用户自写的引用(或组合层默认之外的既有值)不接管:只有我们写过的前值才覆盖。
22
+ if (current !== undefined &&
23
+ current !== previous &&
24
+ current !== DEFAULT_DEEPSEEK_SEARCH_KEY_ENV)
25
+ return { write: null, result: null };
26
+ return {
27
+ write: {
28
+ kind: 'section',
29
+ section: { ...view.user, [WEB_SEARCH_API_KEY_ENV_KEY]: deepseek.apiKeyEnv },
30
+ },
31
+ result: deepseek.apiKeyEnv,
32
+ };
28
33
  });
29
34
  }
30
35
  catch (error) {
31
36
  host.warn(`DeepSeek 网页搜索密钥引用同步失败(${codeOf(error)}),搜索按当前本地配置继续`);
32
37
  return previous;
33
38
  }
34
- return deepseek.apiKeyEnv;
35
39
  }
36
- function webSearchKeyEnvOf(settings) {
37
- const value = sectionValue(settings, WEB_SEARCH_DEEPSEEK_NAMESPACE).apiKeyEnv;
40
+ function keyEnvOf(view) {
41
+ const value = view.value[WEB_SEARCH_API_KEY_ENV_KEY];
38
42
  return typeof value === 'string' && value !== '' ? value : undefined;
39
43
  }
@@ -18,6 +18,14 @@ export function installConfiguration(ctx, options = {}) {
18
18
  // 无 logger 的宿主(单测)忽略。
19
19
  }
20
20
  };
21
+ // 生命周期操作单飞(官方 saveSelection 同款 promise 链):load / 登录 / 登出 / 同步交错时
22
+ // 按到达顺序整段落定——设置事务、credentials 与 saveSelection 三类写由此一并有序。
23
+ let opTail = Promise.resolve();
24
+ const run = (op) => {
25
+ const next = opTail.then(op, op);
26
+ opTail = next.then(() => undefined, () => undefined);
27
+ return next;
28
+ };
21
29
  const ports = createPorts(ctx, warn, options);
22
30
  // api 门面读连接凭据的桥:闭包延迟求值(编排层构造完成后才可能被调用)。
23
31
  const engineRef = { current: undefined };
@@ -34,16 +42,16 @@ export function installConfiguration(ctx, options = {}) {
34
42
  installSessionReporting(orchestrator);
35
43
  void orchestrator
36
44
  .mountDomains()
37
- .then(() => lifecycle.start(orchestrator))
45
+ .then(() => run(() => lifecycle.start(orchestrator)))
38
46
  .catch((error) => {
39
47
  warn(`首次领取失败:${error instanceof Error ? error.message : String(error)}`);
40
48
  });
41
49
  return {
42
50
  config: new ConfigService(orchestrator),
43
51
  session: {
44
- login: input => lifecycle.login(orchestrator, input),
45
- logout: () => lifecycle.logout(orchestrator),
46
- sync: scope => lifecycle.syncByScope(orchestrator, scope),
52
+ login: input => run(() => lifecycle.login(orchestrator, input)),
53
+ logout: () => run(() => lifecycle.logout(orchestrator)),
54
+ sync: scope => run(() => lifecycle.syncByScope(orchestrator, scope)),
47
55
  },
48
56
  health: { snapshot: () => healthSnapshot(orchestrator) },
49
57
  status: () => statusSection(orchestrator),
@@ -1,24 +1,11 @@
1
- /** 本插件自铸的名字形状:路由 `semi2-<32 hex>`、密钥引用 `SEMI2_MODEL_KEY_<大写 hex>`。 */
1
+ /** 本插件自铸的路由名形状:`semi2-<32 hex>`(密钥引用名由 hub/compile 的 connectionApiKeyEnv 自铸)。 */
2
2
  export const SELF_MINTED_ROUTE = /^semi2-[\da-f]{32}$/;
3
- export const SELF_MINTED_KEY_ENV = /^SEMI2_MODEL_KEY_[\dA-F]+$/;
4
3
  /** 本插件在官方 `llm-deepseek` 段表达过的三个键:服务端撤下 DEEPSEEK 连接时一并清掉。 */
5
4
  export const DEEPSEEK_OVERRIDE_KEYS = ['apiKeyEnv', 'baseURL', 'models'];
6
- /** 一个设置段的当前有效值:官方描述符的 `value` 是不透明值,本插件只认对象段,其余按空段处理。 */
7
- export function sectionValue(settings, ns) {
8
- const value = settings.describe().find(entry => entry.ns === ns)?.value;
9
- return isRecord(value) ? value : {};
10
- }
11
5
  /** 设置段是否已可描述(条目 ACTIVE 且声明了 volatile 字段)。 */
12
6
  export function hasSection(settings, ns) {
13
7
  return settings.describe().some(entry => entry.ns === ns);
14
8
  }
15
- /**
16
- * 该段的**用户层**原始内容(只认 `describe()` 的 `user`:组合层与 schema 默认不算用户写过)。
17
- * 写入前用它保住用户写在同一段里的其他键——`replace` 会把用户层整体对齐到给定内容。
18
- */
19
- export function userSection(settings, ns) {
20
- return persistedSection(settings.describe(), ns);
21
- }
22
9
  /**
23
10
  * 该段的**组合层**内容(`describe()` 的 `base`,已带 schema 默认)=部署默认;
24
11
  * 登出撤下默认模型时用它把选择交还给组合层。
@@ -30,8 +17,3 @@ export function baseSection(settings, ns) {
30
17
  function isRecord(value) {
31
18
  return value !== null && typeof value === 'object' && !Array.isArray(value);
32
19
  }
33
- /** 用户层事实(`describe()` 的 `value` 含默认与组合层,`user` 才是用户写下的那一层)。 */
34
- function persistedSection(entries, ns) {
35
- const entry = entries.find(row => row.ns === ns);
36
- return isRecord(entry?.user) ? entry.user : {};
37
- }
@@ -1,31 +1,17 @@
1
- import { setTimeout as delay } from 'node:timers/promises';
2
- import { ConfigurationError } from "../hub/errors.js";
3
- import { createHostWrites } from "./host-writes.js";
1
+ import { transact, } from "./settings-tx.js";
4
2
  const SETTINGS_TIMEOUT_MS = 5_000;
5
3
  const NAMESPACE_WAIT_MS = 2_000;
6
- /** 设置段就绪轮询间隔(纯内部预算,不单列到 Config)。 */
7
- const NAMESPACE_POLL_MS = 50;
4
+ const WRITE_TIMEOUT_MS = 3_000;
8
5
  export function createPorts(ctx, warn, options = {}) {
9
6
  const settingsTimeoutMs = options.settingsTimeoutMs ?? SETTINGS_TIMEOUT_MS;
10
7
  const namespaceWaitMs = options.namespaceWaitMs ?? NAMESPACE_WAIT_MS;
11
- const writes = createHostWrites(warn, {
12
- // 宿主就绪门 = profile 的 loader 条目树:设置写入内部等的就是它。服务缺席(宿主直跑 / 假宿主)立即放行。
13
- ready: () => {
14
- const loader = ctx.get('loader', false);
15
- return Promise.resolve(loader?.await?.());
16
- },
17
- writeTimeoutMs: options.writeTimeoutMs,
18
- });
8
+ const writeTimeoutMs = options.writeTimeoutMs ?? WRITE_TIMEOUT_MS;
9
+ const tx = { warn, namespaceWaitMs, writeTimeoutMs };
19
10
  const service = (key) => new Promise((resolve) => {
20
11
  ctx.inject([key], (injected) => {
21
12
  resolve(injected.get(key));
22
13
  });
23
14
  });
24
- const requireNamespace = async (settings, ns) => {
25
- if (await waitForNamespace(settings, ns, namespaceWaitMs))
26
- return;
27
- throw new ConfigurationError('CONFIG_APPLY_FAILED');
28
- };
29
15
  const hasWorkspaceController = () => ctx.get('workspaceController', false) !== undefined;
30
16
  return {
31
17
  settings: () => {
@@ -43,36 +29,10 @@ export function createPorts(ctx, warn, options = {}) {
43
29
  credentialsNow: () => ctx.get('credentials', false),
44
30
  agentDefaultModel: () => ctx.get('agentDefaultModel', false),
45
31
  hasWorkspaceController,
46
- replaceNamespace: async (settings, ns, section) => {
47
- await requireNamespace(settings, ns);
48
- const replace = settings.replace?.bind(settings);
49
- if (replace === undefined) {
50
- warn(`宿主 settings 无 replace,${ns} 退回合并写入:服务端已撤下的条目可能残留`);
51
- await writes.run(ns, () => settings.update(ns, section));
52
- return;
53
- }
54
- await writes.run(ns, () => replace(ns, section));
55
- },
56
- clearKeys: async (settings, ns, keys) => {
57
- const mutate = settings.mutate?.bind(settings);
58
- if (mutate === undefined) {
59
- warn(`宿主 settings 无 mutate,${ns} 无法按路径删除:服务端已撤销的覆盖可能残留`);
60
- return;
61
- }
62
- await requireNamespace(settings, ns);
63
- const ops = keys.map(key => ({ op: 'unset', path: [key] }));
64
- await writes.run(ns, () => mutate(ns, ops));
65
- },
32
+ replaceNamespace: (settings, ns, plan) => transact(settings, ns, plan, tx),
33
+ clearKeys: (settings, ns, keys) => transact(settings, ns, () => ({
34
+ write: { kind: 'ops', ops: keys.map(key => ({ op: 'unset', path: [key] })) },
35
+ result: undefined,
36
+ }), tx),
66
37
  };
67
38
  }
68
- /** 等设置段可描述(条目 ACTIVE 且声明了 volatile 字段);超时返回 false,不抛。 */
69
- async function waitForNamespace(settings, ns, waitMs) {
70
- const deadline = Date.now() + waitMs;
71
- for (;;) {
72
- if (settings.describe().some(entry => entry.ns === ns))
73
- return true;
74
- if (Date.now() >= deadline)
75
- return false;
76
- await delay(NAMESPACE_POLL_MS);
77
- }
78
- }
@@ -36,7 +36,7 @@ async function scopeOf(engine, cwd) {
36
36
  /** 会话创建:带上工作区与默认模型选择,hub 据此建行。 */
37
37
  async function reportCreated(engine, session) {
38
38
  const scope = await scopeOf(engine, session.header.cwd);
39
- const selection = engine.ctx.get('agentDefaultModel', false)?.currentSelection?.();
39
+ const selection = engine.ports.agentDefaultModel()?.currentSelection?.();
40
40
  await reportEvent(engine, {
41
41
  action: 'SESSION_CREATED',
42
42
  outcome: 'SUCCESS',
@@ -0,0 +1,109 @@
1
+ // @fa-software/semi2-dsh/service/settings-tx —— 官方设置事务:describe 现读 → 带 expectedRevision 写 → 冲突重读重算 → 超时降级。
2
+ // 官方 write 在文件锁内与 Loader 热重载串行(dsh-config-editor 的 edit),跨写者互斥不归本模块;
3
+ // 这里只补官方没有的两样:读改写一致性(CAS + 重算重试)与调用方有界等待。
4
+ // 被超时放弃的那笔仍会照常落定或迟到时被 CAS 拒掉——两种结局都比无条件覆盖安全。
5
+ import { SettingsConflictError } from '@deepseek-ai/dsh-settings';
6
+ import { setTimeout as delay } from 'node:timers/promises';
7
+ import { ConfigurationError } from "../hub/errors.js";
8
+ /** 冲突重试上限(含首次):并发编辑同一段(用户正在设置页写)给两次重算机会。 */
9
+ const MAX_ATTEMPTS = 3;
10
+ /** 设置段就绪轮询间隔(纯内部预算,不单列到 Config)。 */
11
+ const NAMESPACE_POLL_MS = 50;
12
+ /** 到点标记:与「底层真的返回了 undefined」区分开。 */
13
+ const EXPIRED = Symbol('expired');
14
+ /**
15
+ * 一笔官方设置事务。plan 必须是 view 的纯函数(外部输入放闭包里):
16
+ * 冲突重试会用新鲜视图重调它,副作用(记账、集合填充)应以 result 带出而不是改外部状态。
17
+ */
18
+ export async function transact(settings, ns, plan, options) {
19
+ if (!(await waitForNamespace(settings, ns, options.namespaceWaitMs)))
20
+ throw new ConfigurationError('CONFIG_APPLY_FAILED');
21
+ for (let attempt = 1;; attempt += 1) {
22
+ const view = viewOf(settings, ns);
23
+ const outcome = plan(view);
24
+ if (outcome.write == null)
25
+ return outcome.result;
26
+ try {
27
+ const settled = await within(applyWrite(settings, ns, outcome.write, view.revision, options), options.writeTimeoutMs);
28
+ if (settled === EXPIRED) {
29
+ options.warn(`${ns} 设置写入超时(${String(options.writeTimeoutMs)}ms),本端继续,状态可能落后`);
30
+ throw new ConfigurationError('CONFIG_APPLY_FAILED');
31
+ }
32
+ return outcome.result;
33
+ }
34
+ catch (error) {
35
+ if (isConflict(error)) {
36
+ if (attempt < MAX_ATTEMPTS)
37
+ continue;
38
+ options.warn(`${ns} 设置写入连续冲突(${String(MAX_ATTEMPTS)} 次),本轮降级`);
39
+ throw new ConfigurationError('CONFIG_APPLY_FAILED');
40
+ }
41
+ throw error;
42
+ }
43
+ }
44
+ }
45
+ /** 写瞬间视图;宿主不提供 revision 时按 undefined(不带 CAS)透传。 */
46
+ function viewOf(settings, ns) {
47
+ const entry = settings.describe().find(row => row.ns === ns);
48
+ return {
49
+ value: recordOf(entry?.value),
50
+ user: recordOf(entry?.user),
51
+ revision: typeof entry?.revision === 'number' ? entry.revision : undefined,
52
+ };
53
+ }
54
+ function recordOf(value) {
55
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
56
+ ? value
57
+ : {};
58
+ }
59
+ /** 落一笔写:section 走 replace(缺则回退 update 的合并写入)、ops 走 mutate;都带上 CAS。 */
60
+ function applyWrite(settings, ns, write, revision, options) {
61
+ if (write.kind === 'ops') {
62
+ const mutate = settings.mutate?.bind(settings);
63
+ if (mutate === undefined) {
64
+ options.warn(`宿主 settings 无 mutate,${ns} 无法按路径删除:服务端已撤销的覆盖可能残留`);
65
+ return Promise.resolve();
66
+ }
67
+ return mutate(ns, write.ops, revision);
68
+ }
69
+ const replace = settings.replace?.bind(settings);
70
+ if (replace === undefined) {
71
+ options.warn(`宿主 settings 无 replace,${ns} 退回合并写入:服务端已撤下的条目可能残留`);
72
+ return settings.update(ns, write.section, revision);
73
+ }
74
+ return replace(ns, write.section, revision);
75
+ }
76
+ /** 官方冲突(读与写之间有人改了段):按结构判 code,防闭包内模块实例分裂时漏认。 */
77
+ function isConflict(error) {
78
+ return error instanceof SettingsConflictError ||
79
+ (error instanceof Error && error.code === 'SETTINGS_CONFLICT');
80
+ }
81
+ /** 到点返回 {@link EXPIRED},否则原样给出结果;定时器总在返回前清掉。 */
82
+ async function within(promise, ms) {
83
+ let timer;
84
+ try {
85
+ return await Promise.race([
86
+ promise,
87
+ new Promise((resolve) => {
88
+ timer = globalThis.setTimeout(() => {
89
+ resolve(EXPIRED);
90
+ }, ms);
91
+ }),
92
+ ]);
93
+ }
94
+ finally {
95
+ if (timer !== undefined)
96
+ globalThis.clearTimeout(timer);
97
+ }
98
+ }
99
+ /** 等设置段可描述(条目 ACTIVE 且声明了 volatile 字段);超时返回 false,不抛。 */
100
+ export async function waitForNamespace(settings, ns, waitMs) {
101
+ const deadline = Date.now() + waitMs;
102
+ for (;;) {
103
+ if (settings.describe().some(entry => entry.ns === ns))
104
+ return true;
105
+ if (Date.now() >= deadline)
106
+ return false;
107
+ await delay(NAMESPACE_POLL_MS);
108
+ }
109
+ }
@@ -1,5 +1,5 @@
1
1
  import type { DomainHost, DomainService, LoadRun, ModelsPeer, RevertRun } from '../types.ts';
2
- import { type CompiledConfiguration } from '../../hub/compile.ts';
2
+ import type { CompiledConfiguration } from '../../hub/compile.ts';
3
3
  import type { Semi2ConfigState } from '../../hub/facts.ts';
4
4
  export declare class ModelsDomain implements DomainService, ModelsPeer {
5
5
  private readonly host;
@@ -29,11 +29,11 @@ export declare class ModelsDomain implements DomainService, ModelsPeer {
29
29
  * 发行 `llm-pi-ai` 的 `providers` 段:本插件的路由完全对齐服务端现状(含回收),用户自己写的路由原样保留。
30
30
  *
31
31
  * 预设路由键就是 dsh 的提供方 id,与用户手写的路由同名时以用户为准(改用自铸路由),
32
- * 否则会把人家配置里的 `anthropic:` 段整段替换掉。
32
+ * 否则会把人家配置中的 `anthropic:` 段整段替换掉。让位判定看生效值(value 层),
33
+ * 认领回收只认用户层里的自铸名字——组合层条目由官方 `replace` 的 base 合并自行带回。
33
34
  *
34
35
  * @returns 实际落地的路由表(原路由键 → 最终路由键),调用侧据此改写默认模型与项目选择——
35
36
  * 让位后若仍按原路由键选模型,`agent-default-model` 会指向一条属于用户的路由。
36
37
  */
37
38
  private applyProviders;
38
- private readProviders;
39
39
  }
@@ -2,7 +2,7 @@ import type { CompiledDeepSeek } from '../../hub/compile.ts';
2
2
  import type { DomainHost, LoadRun } from '../types.ts';
3
3
  /**
4
4
  * 官方搜索默认解析 `DEEPSEEK_API_KEY`,而 hub 下发的是连接级引用;这里只对齐引用,
5
- * 搜索端点与其他搜索配置仍归本地配置的所有者。
5
+ * 搜索端点与其他搜索配置仍归本地配置的所有者。接管判定用写瞬间的生效值(官方事务内)。
6
6
  * @returns 本进程写进该段的引用;null = 没写或已交还用户。
7
7
  */
8
8
  export declare function syncWebSearchKeyEnv(host: DomainHost, run: LoadRun, deepseek: CompiledDeepSeek | null, previous: string | null): Promise<string | null>;
@@ -1,7 +1,7 @@
1
1
  import type { InputModality, ModelProtocol, ReasoningLevel, RuntimeBootQuery, RuntimeWorkspaceProjectsQuery } from '../api/generated/graphql.ts';
2
2
  /**
3
3
  * 生效可用目录的一条(连接启用 ∧ 模型启用 ∧ 存在性 ∧ 密钥齐备)。
4
- * 根字段可空:取数失败只让该字段为 null,故取 NonNullable 元素类型,null 由 runtime.ts 归一为空集合。
4
+ * 根字段可空:取数失败只让该字段为 null(形状见 api/index.ts 的 RuntimeBootFacts),各领域按降级处理。
5
5
  */
6
6
  export type RuntimeModel = NonNullable<RuntimeBootQuery['runtimeModels']>[number];
7
7
  export type RuntimeConnection = RuntimeModel['connection'];
@@ -1,18 +1,10 @@
1
- import type { SettingsLike } from './ports.ts';
2
- /** 本插件自铸的名字形状:路由 `semi2-<32 hex>`、密钥引用 `SEMI2_MODEL_KEY_<大写 hex>`。 */
1
+ import type { SettingsLike } from './settings-tx.ts';
2
+ /** 本插件自铸的路由名形状:`semi2-<32 hex>`(密钥引用名由 hub/compile 的 connectionApiKeyEnv 自铸)。 */
3
3
  export declare const SELF_MINTED_ROUTE: RegExp;
4
- export declare const SELF_MINTED_KEY_ENV: RegExp;
5
4
  /** 本插件在官方 `llm-deepseek` 段表达过的三个键:服务端撤下 DEEPSEEK 连接时一并清掉。 */
6
5
  export declare const DEEPSEEK_OVERRIDE_KEYS: readonly ["apiKeyEnv", "baseURL", "models"];
7
- /** 一个设置段的当前有效值:官方描述符的 `value` 是不透明值,本插件只认对象段,其余按空段处理。 */
8
- export declare function sectionValue(settings: SettingsLike, ns: string): Record<string, unknown>;
9
6
  /** 设置段是否已可描述(条目 ACTIVE 且声明了 volatile 字段)。 */
10
7
  export declare function hasSection(settings: SettingsLike, ns: string): boolean;
11
- /**
12
- * 该段的**用户层**原始内容(只认 `describe()` 的 `user`:组合层与 schema 默认不算用户写过)。
13
- * 写入前用它保住用户写在同一段里的其他键——`replace` 会把用户层整体对齐到给定内容。
14
- */
15
- export declare function userSection(settings: SettingsLike, ns: string): Record<string, unknown>;
16
8
  /**
17
9
  * 该段的**组合层**内容(`describe()` 的 `base`,已带 schema 默认)=部署默认;
18
10
  * 登出撤下默认模型时用它把选择交还给组合层。
@@ -1,15 +1,9 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import type { CredentialProvider } from '@deepseek-ai/dsh-credentials';
3
3
  import type { AgentDefaultModelConfig } from '@deepseek-ai/dsh-agent-default-model';
4
- import type { SettingsForms } from '@deepseek-ai/dsh-settings';
5
4
  import type { ModelSelection } from '../hub/compile.ts';
6
- /**
7
- * 本插件消费的 host settings 服务接口:读 / 写口径照官方 `SettingsForms`。
8
- * 设置段 = Profile 插件条目 id,`describe()` 只列「光纤 ACTIVE 且 Config 声明了 volatile 字段」的条目,
9
- * 因此磁盘清理写与读都要先等目标条目可描述(内部 `requireNamespace`);
10
- * `replace` / `mutate` / `configure` 是可缺的降级口。
11
- */
12
- export type SettingsLike = Pick<SettingsForms, 'describe' | 'update'> & Partial<Pick<SettingsForms, 'replace' | 'mutate' | 'configure'>>;
5
+ import { type SettingsLike, type SettingsView, type TxPlan } from './settings-tx.ts';
6
+ export type { SettingsLike, SettingsView, TxPlan } from './settings-tx.ts';
13
7
  /**
14
8
  * 本插件消费的 dsh agentDefaultModel 服务接口(默认模型与无头角色选择)。
15
9
  * 读口取自官方 `AgentDefaultModelConfig`;写入口径不变,档位是 hub 下发的普通字符串
@@ -18,18 +12,12 @@ export type SettingsLike = Pick<SettingsForms, 'describe' | 'update'> & Partial<
18
12
  export type AgentDefaultModelLike = Partial<Pick<AgentDefaultModelConfig, 'currentSelection'>> & {
19
13
  saveSelection(next: ModelSelection): Promise<void>;
20
14
  };
21
- /**
22
- * 本插件消费的 dsh loader 服务接口:`await()` 等整棵树空闲(设置写入的宿主就绪门)。
23
- */
24
- export interface LoaderLike {
25
- await?: (() => Promise<unknown>) | undefined;
26
- }
27
15
  export interface PortsOptions {
28
16
  /** 等 settings 服务的上限(ms)。 */
29
17
  settingsTimeoutMs?: number | undefined;
30
18
  /** 等设置段可读可写的上限(ms)。 */
31
19
  namespaceWaitMs?: number | undefined;
32
- /** 单次写盘的上限(ms;缺省见 host-writes)。 */
20
+ /** 单次写盘的上限(ms;缺省见 settings-tx)。 */
33
21
  writeTimeoutMs?: number | undefined;
34
22
  }
35
23
  export interface Ports {
@@ -44,13 +32,12 @@ export interface Ports {
44
32
  /** workspaceController 的即时宽松读取(缺席 = headless 应用,走隐式工作区)。 */
45
33
  hasWorkspaceController(): boolean;
46
34
  /**
47
- * 权威写入:把该条目的用户层整体对齐到给定内容。
48
- * `update` 是**递归合并**(dsh-settings 的 mergeLayers),只增不减——服务端撤掉的条目
49
- * 会在本地留成残渣,provider 路由尤其致命:目录里已不存在的模型继续可选。
50
- * `replace` 先回到组合层再合并给定字段,所以只用于本插件全权决定的段(`llm-pi-ai` 的 `providers`)。
35
+ * 权威写入(官方设置事务):plan 拿写瞬间的 value / user / revision,冲突自动重算重试。
36
+ * `replace` 语义 = base ⊕ section,只用于本插件全权决定的段(`llm-pi-ai` 的 `providers`、
37
+ * `llm-deepseek` 的三键、`web-search-deepseek` 的 `apiKeyEnv`);别的键用 `view.user` 原样带回。
51
38
  */
52
- replaceNamespace(settings: SettingsLike, ns: string, section: object): Promise<void>;
53
- /** 按路径删除本插件写过的键(合并写入删不掉;命中组合层的键会回继承该值)。 */
39
+ replaceNamespace<T>(settings: SettingsLike, ns: string, plan: (view: SettingsView) => TxPlan<T>): Promise<T>;
40
+ /** 按路径删除本插件写过的键(官方 mutate;命中组合层的键会回继承该值)。 */
54
41
  clearKeys(settings: SettingsLike, ns: string, keys: readonly string[]): Promise<void>;
55
42
  }
56
43
  export declare function createPorts(ctx: Context, warn: (message: string) => void, options?: PortsOptions): Ports;
@@ -1,10 +1,13 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  export type { Session } from '@deepseek-ai/dsh-session';
3
3
  import { type AuditEngine } from './audit.ts';
4
+ import type { Ports } from './ports.ts';
4
5
  import type { WorkspaceEntry } from '../hub/workspace-book.ts';
5
6
  /** 会话上报实际消费的编排事实与工作区 peer。 */
6
7
  export interface SessionEngine extends AuditEngine {
7
8
  ctx: Context;
9
+ /** 默认模型读口(经 ports 的统一宽松查找,不另碰 ctx.get)。 */
10
+ ports: Pick<Ports, 'agentDefaultModel'>;
8
11
  workspace(): {
9
12
  tracked(): WorkspaceEntry[];
10
13
  };
@@ -0,0 +1,42 @@
1
+ import type { SettingsForms, SettingsPathOp } from '@deepseek-ai/dsh-settings';
2
+ /**
3
+ * 本插件消费的 host settings 服务接口:读 / 写口径照官方 `SettingsForms`。
4
+ * 设置段 = Profile 插件条目 id,`describe()` 只列「光纤 ACTIVE 且 Config 声明了 volatile 字段」的条目,
5
+ * 因此写入前要先等目标条目可描述;`replace` / `mutate` 是可缺的降级口。
6
+ */
7
+ export type SettingsLike = Pick<SettingsForms, 'describe' | 'update'> & Partial<Pick<SettingsForms, 'replace' | 'mutate' | 'configure'>>;
8
+ /** 一次事务里 plan 拿到的写瞬间视图:生效值 / 用户层 / 官方修订号。 */
9
+ export interface SettingsView {
10
+ /** 该段当前有效值(官方 value:schema 默认 → 组合层 → 用户层)。 */
11
+ value: Record<string, unknown>;
12
+ /** 该段的用户层原始内容(`describe()` 的 `user`):保住别人写在同一段里的键靠它。 */
13
+ user: Record<string, unknown>;
14
+ /** 官方修订号(宿主不支持时 undefined = 不带 CAS 写)。 */
15
+ revision: number | undefined;
16
+ }
17
+ /** 事务的一次产出:`write` 为 null 表示无需写盘、只取 result。 */
18
+ export interface TxPlan<T> {
19
+ write: {
20
+ kind: 'section';
21
+ section: object;
22
+ } | {
23
+ kind: 'ops';
24
+ ops: readonly SettingsPathOp[];
25
+ } | null;
26
+ /** 事务产出(冲突重算后以最后一次 plan 为准)。 */
27
+ result: T;
28
+ }
29
+ export interface TransactOptions {
30
+ warn(message: string): void;
31
+ /** 等设置段可描述的上限(ms)。 */
32
+ namespaceWaitMs: number;
33
+ /** 单次写盘的上限(ms):到点调用方降级,那笔仍在官方锁内照常落定。 */
34
+ writeTimeoutMs: number;
35
+ }
36
+ /**
37
+ * 一笔官方设置事务。plan 必须是 view 的纯函数(外部输入放闭包里):
38
+ * 冲突重试会用新鲜视图重调它,副作用(记账、集合填充)应以 result 带出而不是改外部状态。
39
+ */
40
+ export declare function transact<T>(settings: SettingsLike, ns: string, plan: (view: SettingsView) => TxPlan<T>, options: TransactOptions): Promise<T>;
41
+ /** 等设置段可描述(条目 ACTIVE 且声明了 volatile 字段);超时返回 false,不抛。 */
42
+ export declare function waitForNamespace(settings: SettingsLike, ns: string, waitMs: number): Promise<boolean>;
@@ -3,7 +3,7 @@
3
3
  * 时由 `packages/semi2-dsh/test/unit/dsh-contract.test.ts` 核对该串仍等于客户端 bundle 里的字面量;
4
4
  * 陈旧值只退化为原生行为。
5
5
  */
6
- export declare const WELCOME_NOTICE_VERSION = "2026-08-13.1";
6
+ export declare const WELCOME_NOTICE_VERSION = "2026-09-28.1";
7
7
  /** 产品级 GUI onboarding 事实的 durable settings 段(= Profile 条目 id)。 */
8
8
  export declare const WELCOME_NOTICE_NAMESPACE = "ui-settings-general";
9
9
  /** 记录已确认的欢迎声明文案版本。 */
@@ -5,7 +5,7 @@
5
5
  * 时由 `packages/semi2-dsh/test/unit/dsh-contract.test.ts` 核对该串仍等于客户端 bundle 里的字面量;
6
6
  * 陈旧值只退化为原生行为。
7
7
  */
8
- export const WELCOME_NOTICE_VERSION = '2026-08-13.1';
8
+ export const WELCOME_NOTICE_VERSION = '2026-09-28.1';
9
9
  /** 产品级 GUI onboarding 事实的 durable settings 段(= Profile 条目 id)。 */
10
10
  export const WELCOME_NOTICE_NAMESPACE = 'ui-settings-general';
11
11
  /** 记录已确认的欢迎声明文案版本。 */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fa-software/semi2-dsh",
3
- "version": "0.0.16",
3
+ "version": "0.0.17-preview.20260929054409",
4
4
  "description": "Semi2 单插件:semi2-hub 配置引导 + 公共 Web 能力。",
5
5
  "homepage": "https://cnb.cool/fa-software/semi2/dsh/dsh-mono",
6
6
  "bugs": "https://cnb.cool/fa-software/semi2/dsh/dsh-mono/-/issues",
@@ -31,7 +31,8 @@
31
31
  "registry": "https://registry.npmjs.org/"
32
32
  },
33
33
  "devDependencies": {
34
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.7-rc.2",
34
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.1",
35
+ "@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
35
36
  "@fa-software/test-kit": "0.0.0",
36
37
  "@graphql-codegen/cli": "7.4.0",
37
38
  "@graphql-codegen/typescript-operations": "6.1.6",
@@ -44,16 +45,16 @@
44
45
  },
45
46
  "peerDependencies": {
46
47
  "@deepseek-ai/cordis": "~4.0.4",
47
- "@deepseek-ai/dsh-agent-default-model": "^0.1.7-rc.2",
48
- "@deepseek-ai/dsh-api-workspace-controller": "^0.1.7-rc.2",
49
- "@deepseek-ai/dsh-client-connection": "^0.1.7-rc.2",
50
- "@deepseek-ai/dsh-credentials": "^0.1.7-rc.2",
51
- "@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.2",
52
- "@deepseek-ai/dsh-mcp-client": "^0.1.7-rc.2",
53
- "@deepseek-ai/dsh-session": "^0.1.7-rc.2",
54
- "@deepseek-ai/dsh-settings": "^0.1.7-rc.2",
55
- "@deepseek-ai/dsh-skill": "^0.1.7-rc.2",
56
- "@deepseek-ai/dsh-system-prompt": "^0.1.7-rc.2",
48
+ "@deepseek-ai/dsh-agent-default-model": "^0.2.0-rc.1",
49
+ "@deepseek-ai/dsh-api-workspace-controller": "^0.2.0-rc.1",
50
+ "@deepseek-ai/dsh-client-connection": "^0.2.0-rc.1",
51
+ "@deepseek-ai/dsh-credentials": "^0.2.0-rc.1",
52
+ "@deepseek-ai/dsh-host-webserver": "^0.2.0-rc.1",
53
+ "@deepseek-ai/dsh-mcp-client": "^0.2.0-rc.1",
54
+ "@deepseek-ai/dsh-session": "^0.2.0-rc.1",
55
+ "@deepseek-ai/dsh-settings": "^0.2.0-rc.1",
56
+ "@deepseek-ai/dsh-skill": "^0.2.0-rc.1",
57
+ "@deepseek-ai/dsh-system-prompt": "^0.2.0-rc.1",
57
58
  "@deepseek-ai/schemastery": "~3.18.4"
58
59
  },
59
60
  "peerDependenciesMeta": {