/** * 设置管理:策略(标签)配置 * * 设计约束:产品约束-002/003/004(标签体系、标签自定义 + 显示开关、删除份额回未分配) * 通过 ctx.settings.register('one-divine-lot', schema) 注册策略配置 namespace。 * * 数据结构: * strategies: [{ id, name }](R-011 收窄:visible/order 归 tabs 统一管理) * tabs: [{ id, kind: builtin|strategy, refKey|refId, name, visible, order }] * 预置:网格超市、手动做T(R-003 O3-5:可删除,彻底自由) * * R-011 迭代 09(产品约束-009、技术约束-014): * - settings.tabs 从布尔对象升级为统一有序数组(内置 + 策略混排) * - 旧格式读取时静默归一化迁移(旧隐藏策略迁移后显示,Q3) * - addStrategy 联动追加 tab(末尾,Q2);removeStrategy 联动删除对应 tab(D3) * - 策略行 name 由 getTabs join strategies(改名自动跟随,Q4) * - removeStrategy 的份额清理由上层(api.js 编排 storage)完成 * * R-027(迭代 22 阶段一)策略 tab 静态化: * - 策略集合与名称固定为内置两项(BUILTIN_STRATEGIES):不可新增/删除/重命名 * - tab 列表由 内置 tab + 内置策略 tab 常量派生;存量 tabs 仅保留 visible/order 偏好 * - 字段定义真相源迁到 strategyFields;strategies[] 收窄为身份表(仅兼容读取旧 configSchema) * - 退役 addStrategy/removeStrategy/renameStrategy/updateStrategies/append|removeStrategyTab */ import z from '@deepseek-ai/schemastery'; /** 内置 tab 元数据(常量,顺序即默认顺序;R-011) */ export const BUILTIN_TABS = [ { id: 'tab-all-positions', refKey: 'allPositions', name: '全部持仓' }, { id: 'tab-trade-records', refKey: 'tradeRecords', name: '交易记录' }, { id: 'tab-watchlist', refKey: 'watchlist', name: '关注列表' }, ]; /** 内置策略(R-027 静态化:集合与名称固定,不可增删改名;字段定义另存 strategyFields) */ export const BUILTIN_STRATEGIES = [ { id: 'grid-supermarket', name: '网格超市' }, { id: 'manual-t', name: '手动做T' }, ]; /** 内置策略 id 判定(字段定义写入 / 校验用) */ export function isBuiltinStrategy(strategyId) { return BUILTIN_STRATEGIES.some((s) => s.id === strategyId); } /** 未知策略 id 直接抛错(写路径守卫) */ export function assertBuiltinStrategy(strategyId) { if (!isBuiltinStrategy(strategyId)) { throw Object.assign(new Error('未知策略: ' + strategyId), { code: 'strategy-not-found' }); } } /** 默认 tab 列表(首次注册时的初始值;R-027:内置 tab + 内置策略 tab) */ export const DEFAULT_TABS = [ ...BUILTIN_TABS.map((t, i) => ({ id: t.id, kind: 'builtin', refKey: t.refKey, name: t.name, visible: true, order: i })), ...BUILTIN_STRATEGIES.map((s, i) => ({ id: 'tab-strategy-' + s.id, kind: 'strategy', refId: s.id, name: s.name, visible: true, order: BUILTIN_TABS.length + i, })), ]; /** * 策略持仓表基础数据列目录(迭代 11 列显隐扩展) * 可配置区基础列:key / label / 默认是否显示(data=固定数据列;自定义字段列由 configSchema 动态合成) * 固定列(不在此目录,锁死):展开箭头 | 代码 | 名称(最前)| 操作(最后) */ export const COLUMN_META = [ { key: 'lastPrice', label: '现价', fixed: false }, { key: 'pctChange', label: '涨幅', fixed: false }, { key: 'lastClose', label: '昨收', fixed: false }, { key: 'avgPrice', label: '成本价', fixed: false }, { key: 'lastTradePrice', label: '最后一笔成交价', fixed: false }, { key: 'shares', label: '策略份额', fixed: false }, // R-015/迭代 13:行情列(盘口快照 + 合约信息;defaultVisible=false 默认隐藏,列设置勾选开启) { key: 'upStopPrice', label: '涨停价', fixed: false, defaultVisible: false }, { key: 'downStopPrice', label: '跌停价', fixed: false, defaultVisible: false }, { key: 'open', label: '今开', fixed: false, defaultVisible: false }, { key: 'high', label: '最高', fixed: false, defaultVisible: false }, ]; /** * 策略自定义字段定义 schema(R-013 四类型 + R-026 计算类型) * - type='formula' 时使用 formula(中文变量表达式串)+ decimals(显示小数位),不使用 def */ const fieldSchema = z.object({ key: z.string().required(), // 稳健标识(英文 slug,与字段值 JSON 的 key 对齐) label: z.string(), // UI 展示名(可中文;计算字段中并作公式变量名) type: z.union([ z.const('text'), z.const('number'), z.const('boolean'), z.const('enum'), z.const('formula'), // R-026:计算字段(值由公式实时算出,不落库) ]).required(), enum: z.array(z.string()).default([]), // 仅 type=enum:预设选项 def: z.any(), // 默认值(text=string / number=number / boolean=boolean / enum=枚举项) unit: z.string().default(''), // 单位(可为空;如 % / 元 / 手 —— 展示时拼在值后) formula: z.string().default(''), // 仅 type=formula:表达式串(中文变量) resultKind: z.union([z.const('number'), z.const('boolean')]).default('number'), // R-028:数字 / 判定 decimals: z.number().default(2), // 仅 type=formula 且 resultKind=number:显示小数位(0-4) }); /** 策略设置 schema(schemastery 定义,settings.register 要求 schema 对象) */ /** @type {import('@deepseek-ai/schemastery').Schema} 策略设置 schema(显式注解:含 z.dict,规避 dts 推断 cosmokit Dict 引用) */ export const strategySchema = z.object({ // 策略定义表(R-011 收窄 + R-013 扩展 configSchema:id + name + 自定义字段定义) // R-013(技术约束-015):configSchema = 该策略需要的自定义字段定义(key/label/type/enum/def), // 随策略定义存 settings 不落库;旧策略缺省 [](兼容,Q4)。 strategies: z.array(z.object({ id: z.string().required(), name: z.string().required(), configSchema: z.array(fieldSchema).default([]), })).default([]), // R-027:字段定义真相源(写路径唯一入口;strategies[].configSchema 仅兼容读取旧数据) strategyFields: z.dict(z.array(fieldSchema).default([])).default({}), // 统一 tab 列表(R-011:唯一顺序与显隐来源;内置 + 策略混排) // union 兼容存量:旧格式为布尔对象(迭代 02),新格式为有序数组;normalizeTabs 读取时归一化 tabs: z.union([ z.object({ allPositions: z.boolean().default(true), tradeRecords: z.boolean().default(true), watchlist: z.boolean().default(true), }), z.array(z.object({ id: z.string().required(), kind: z.union([z.const('builtin'), z.const('strategy')]).required(), refKey: z.string(), // builtin 专用:allPositions/tradeRecords/watchlist(可空) refId: z.string(), // strategy 专用:策略 id(可空) name: z.string(), // 展示名(策略行 join 时刷新,Q4 自动跟随改名;可空) visible: z.boolean().default(true), order: z.number().default(0), })), ]).default(DEFAULT_TABS), // QMT 连接配置(R-004,技术约束-008):list + 激活态 + 默认态 qmtConnections: z.object({ list: z.array(z.object({ id: z.string().required(), name: z.string().required(), baseUrl: z.string().required(), order: z.number().default(0), })).default([]), activeId: z.string().default(''), defaultId: z.string().default(''), }).default({ list: [], activeId: '', defaultId: '' }), // 策略持仓表列配置(迭代 11 扩展:每策略一套,仅存与默认不同的覆盖;读取归一化) // strategyColumns: { [strategyId]: [{ key, visible }] } —— key 覆盖 COLUMN_META 基础列 + 该策略 configSchema 字段列 strategyColumns: z.dict( z.array(z.object({ key: z.string().required(), visible: z.boolean().default(true), })).default([]) ).default({}), }); /** 简易拼音映射表(常用汉字 → 拼音,用于 slug 生成兜底) */ const PINYIN_MAP = { // 完整短语映射(优先级最高) '网格超市': 'grid-supermarket', '手动做T': 'manual-t', '长线持有': 'long-term-hold', '短线交易': 'short-term-trade', '波段操作': 'swing-trade', '价值投资': 'value-invest', '趋势跟踪': 'trend-follow', '新股申购': 'ipo-subscribe', '打板': 'board', '低吸': 'dip-buy', '高抛': 'high-sell', '做T': 't', // 词映射 '长线': 'long-term', '短线': 'short-term', '持有': 'hold', '波段': 'swing', '价值': 'value', '网格': 'grid', '策略': 'strategy', '趋势': 'trend', '新股': 'new-stock', '打新': 'ipo', // 单字映射 '超': 'super', '市': 'market', '场': 'market', '手': 'manual', '动': 'action', '长': 'long', '短': 'short', '持': 'hold', '波': 'swing', '值': 'value', '格': 'grid', '略': 'strategy', '趋': 'trend', '势': 'trend', }; /** * 生成策略英文 id(slug) * 规则:常见中文策略名映射拼音 → 去空格/转小写/连字符 → 非 ASCII 字符用序号兜底 * @param {string} name 策略名称 * @param {string[]} existingIds 已存在的 id(用于去重) * @returns {string} 唯一英文 id */ export function generateStrategyId(name, existingIds = []) { const raw = String(name ?? '').trim(); if (!raw) return ''; // 1. 整名映射(常见策略名直接映射,优先级最高) const whole = PINYIN_MAP[raw]; if (whole) return dedupe(whole, existingIds); // 2. 最长子串贪婪匹配(把名字切成已映射的词/字片段) let slug = ''; let i = 0; while (i < raw.length) { let matched = false; for (let len = Math.min(4, raw.length - i); len >= 1; len--) { const seg = raw.slice(i, i + len); const p = PINYIN_MAP[seg]; if (p) { slug += p + '-'; i += len; matched = true; break; } } if (matched) continue; const ch = raw[i]; if (/[a-zA-Z0-9]/.test(ch)) { slug += ch.toLowerCase() + '-'; } i++; } let base = slug.replace(/-+$/, '').replace(/^-+/, '') || 'strategy'; // 3. 规范化(去空格、连字符规整、移除非法字符) base = base .replace(/\s+/g, '-') .replace(/[^a-z0-9-]/g, '') .replace(/-+/g, '-') .replace(/^-|-$/g, '') || 'strategy'; return dedupe(base, existingIds); } /** 去重:若 id 已存在,追加序号 */ function dedupe(base, existingIds) { let candidate = base; let i = 2; while (existingIds.includes(candidate)) { candidate = `${base}-${i}`; i++; } return candidate; } /** * 注册策略设置 namespace,返回 SettingsScope * @param {import('@deepseek-ai/cordis').Context} ctx */ export function registerSettings(ctx, config) { const ns = config.settingsNamespace ?? 'one-divine-lot'; const scope = ctx.settings.register(ns, strategySchema, { // 组合 base(如需从 composition 注入默认值) base: config.strategies ? { strategies: config.strategies } : undefined, }); ctx.logger?.info(`[one-divine-lot] 设置已注册: ${ns}`); return scope; } /** 读取 settings 快照(容错:测试用桩 scope 无 get 时返回空对象) */ function scopeValue(scope) { return (scope && typeof scope.get === 'function') ? (scope.get() ?? {}) : {}; } /** 字段类型白名单(含 R-026 计算类型) */ const FIELD_TYPES = new Set(['text', 'number', 'boolean', 'enum', 'formula']); /** * 字段定义归一化(R-013 + R-026):类型白名单、枚举数组、公式串、小数位夹取 0-4 * @param {Array} fields configSchema 原始数组 * @returns {Array} 归一化后的字段定义数组 */ export function normalizeFields(fields) { return (Array.isArray(fields) ? fields : []).map((f) => { const type = FIELD_TYPES.has(f?.type) ? f.type : 'text'; const out = { key: String(f?.key ?? ''), label: String(f?.label ?? ''), type, enum: Array.isArray(f?.enum) ? f.enum.map((x) => String(x)) : [], unit: String(f?.unit ?? ''), }; if (type === 'formula') { out.formula = String(f?.formula ?? '').trim(); out.resultKind = f?.resultKind === 'boolean' ? 'boolean' : 'number'; // R-028 const n = Number(f?.decimals); out.decimals = Number.isFinite(n) ? Math.min(4, Math.max(0, Math.round(n))) : 2; } else if (f?.def !== undefined) { out.def = f.def; } return out; }); } /** * 读取策略表(R-027 静态化:恒为内置两项;configSchema 取 strategyFields,缺失时兼容读取旧 strategies[]) * @param {ReturnType} scope * @returns {Array<{id: string, name: string, configSchema: Array}>} */ export function getStrategies(scope) { const value = scopeValue(scope); const legacy = new Map((Array.isArray(value.strategies) ? value.strategies : []).map((s) => [s.id, s.configSchema])); const store = (value.strategyFields && typeof value.strategyFields === 'object') ? value.strategyFields : {}; return BUILTIN_STRATEGIES.map((s) => ({ ...s, configSchema: normalizeFields( Array.isArray(store[s.id]) ? store[s.id] : (Array.isArray(legacy.get(s.id)) ? legacy.get(s.id) : []), ), })); } /** 读取某策略字段定义(未知名 → []) */ export function getStrategyFields(scope, strategyId) { return getStrategies(scope).find((s) => s.id === strategyId)?.configSchema ?? []; } /** * 写入某策略字段定义(R-027:单策略写入,唯一写路径;不触碰 strategies[] 与份额数据) * @param {ReturnType} scope * @param {string} strategyId 内置策略 id * @param {Array} configSchema 完整字段定义数组 * @returns {Promise} 更新后的策略对象 */ export async function updateStrategyFields(scope, strategyId, configSchema) { assertBuiltinStrategy(strategyId); const cur = scopeValue(scope); const store = (cur.strategyFields && typeof cur.strategyFields === 'object') ? cur.strategyFields : {}; await scope.update({ ...cur, strategyFields: { ...store, [strategyId]: normalizeFields(configSchema) } }); return getStrategies(scope).find((s) => s.id === strategyId); } /** * 归一化 tabs(R-027 静态化):序列由常量派生(内置 tab + 内置策略 tab), * 存量 tabs 仅保留 visible/order 偏好;未知条目(自建策略 tab / 已退役 tab)丢弃,缺失条目补齐。 * 旧布尔对象格式(迭代 02)继续兼容:仅取内置显隐。 * @param {ReturnType} scope * @returns {Array} 归一化后的 tabs 数组(按 order 排序) */ export function normalizeTabs(scope) { const raw = scopeValue(scope).tabs; // 旧布尔对象(迭代 02):映射到内置 tab 显隐 if (raw && typeof raw === 'object' && !Array.isArray(raw)) { return DEFAULT_TABS.map((t) => (t.kind === 'builtin' && t.refKey in raw ? { ...t, visible: raw[t.refKey] !== false } : t)); } if (!Array.isArray(raw)) return DEFAULT_TABS.map((t) => ({ ...t })); const byId = new Map(raw.map((t) => [t.id, t])); return DEFAULT_TABS .map((t) => { const hit = byId.get(t.id); return hit ? { ...t, visible: hit.visible !== false, order: typeof hit.order === 'number' ? hit.order : t.order } : { ...t }; }) .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); } /** * 读取统一 tab 列表(R-011/R-027):归一化结果(策略行 name 恒为内置名,无改名场景) * @param {ReturnType} scope * @returns {Array} tabs 数组(按 order 排序) */ export function getTabs(scope) { return normalizeTabs(scope); } /** * 更新统一 tab 列表(整表更新:顺序 + 显隐;R-011) * 策略行 name 由服务端 join 刷新,客户端可只传 id/order/visible 的数组。 * @param {ReturnType} scope * @param {Array} tabs 完整 tabs 数组 */ export async function updateTabs(scope, tabs) { const current = scope?.get() ?? {}; const known = new Set(DEFAULT_TABS.map((t) => t.id)); const normalized = (Array.isArray(tabs) ? tabs : []) .filter((t) => known.has(t?.id)) // R-027:只接受内置条目(策略条目恒存在,不可增删) .map((t, i) => ({ id: t.id, kind: t.kind === 'strategy' ? 'strategy' : 'builtin', refKey: t.refKey, refId: t.refId, name: t.name, visible: t.visible !== false, order: typeof t.order === 'number' ? t.order : i, })); await scope.update({ ...current, tabs: normalized }); } /** * 读取通用 tab 显隐配置(R-011 迁移期兼容:返回布尔对象,供旧调用点使用) * @deprecated 由 getTabs 取代 */ export function getTabConfig(scope) { const tabs = getTabs(scope); const cfg = { allPositions: true, tradeRecords: true, watchlist: true }; for (const t of tabs) { if (t.kind === 'builtin' && t.refKey in cfg) cfg[t.refKey] = t.visible !== false; } return cfg; } /** * 更新通用 tab 显隐配置(R-011 迁移期兼容:写回统一数组) * @deprecated 由 updateTabs 取代 */ export async function updateTabConfig(scope, tabs) { const currentTabs = getTabs(scope); const next = currentTabs.map((t) => (t.kind === 'builtin' && t.refKey in tabs ? { ...t, visible: tabs[t.refKey] !== false } : t)); await updateTabs(scope, next); } /** * ============================================================================ * 策略持仓表列配置(迭代 11 列显隐扩展;产品约束-011、技术约束-017 待落) * 数据:settings.strategyColumns = { [strategyId]: [{key, visible}] }(仅存覆盖) * 归一化:基础列(COLUMN_META 默认序全显)+ 该策略 configSchema 字段列(configSchema 序全显) * → 应用 strategyColumns 覆盖(visible + 顺序)→ 返回有序列数组 [{key, label, fixed, visible}] * 固定列不在此配置:展开箭头/代码/名称(最前)、操作(最后)。 */ /** * 归一化某策略的列配置(基础列 + 自定义字段列,应用覆盖) * @param {ReturnType} scope * @param {string} strategyId * @returns {Array<{key: string, label: string, fixed: boolean, visible: boolean, kind: 'base'|'field'}>} */ export function normalizeStrategyColumns(scope, strategyId) { const value = scopeValue(scope); // R-027:字段定义真相源 = strategyFields(缺失时兼容读取旧 strategies[].configSchema) const configSchema = getStrategyFields(scope, strategyId); const raw = (value.strategyColumns && value.strategyColumns[strategyId]) || []; // 1. 默认全列(基础列默认序 + 字段列 configSchema 序;显隐缺省跟随 defaultVisible,R-015 行情列默认隐藏) const baseCols = COLUMN_META.map((c) => ({ key: c.key, label: c.label, fixed: false, kind: 'base', defaultVisible: c.defaultVisible !== false })); const fieldCols = configSchema.map((f) => ({ key: f.key, label: f.label || f.key, fixed: false, kind: 'field', type: f.type })); const defaults = [...baseCols, ...fieldCols]; // 2. 应用覆盖:visible + 顺序(未覆盖的列保持默认,追加在默认序后) const override = Array.isArray(raw) ? raw : []; const visibleMap = new Map(); for (const o of override) visibleMap.set(o.key, o.visible !== false); // 覆盖中的自定义字段列若 configSchema 已删除 → 忽略(自动跟随删除) const activeKeys = new Set(defaults.map((c) => c.key)); const validOverride = override.filter((o) => activeKeys.has(o.key)); // 3. 组装:先按覆盖顺序排,未覆盖列追加默认序 const ordered = []; const placed = new Set(); for (const o of validOverride) { const d = defaults.find((c) => c.key === o.key); if (d) { ordered.push({ ...d, visible: o.visible !== false }); placed.add(d.key); } } for (const d of defaults) { if (!placed.has(d.key)) { const dv = visibleMap.has(d.key) ? visibleMap.get(d.key) : d.defaultVisible !== false; ordered.push({ ...d, visible: dv }); } } return ordered; } /** 读取某策略列配置(归一化结果) */ export function getStrategyColumns(scope, strategyId) { return normalizeStrategyColumns(scope, strategyId); } /** * 更新某策略列配置(整表覆盖该策略的 columns) * @param {ReturnType} scope * @param {string} strategyId * @param {Array} columns 完整列数组 [{key, visible}](含不可见列——保留位置,visible 控制显示) */ export async function updateStrategyColumns(scope, strategyId, columns) { const current = scopeValue(scope); const store = current.strategyColumns && typeof current.strategyColumns === 'object' ? current.strategyColumns : {}; const next = { ...store, [strategyId]: (Array.isArray(columns) ? columns : []).map((c) => ({ key: c.key, visible: c.visible !== false })) }; await scope.update({ ...current, strategyColumns: next }); } /** * ============================================================================ * QMT 连接配置(R-004 已定稿;产品约束-005、技术约束-008) * * 数据结构:qmtConnections: { list: [{id,name,baseUrl,order}], activeId, defaultId } * - 激活:一次只能激活一个(activeId),激活配置为当前生效连接,切换立即生效(Q2) * - 默认:启动时自动激活默认配置(Q1),重启回到默认;运行中手动激活立即生效 * - 启动兜底:列表为空时回退 cordis 注入的 qmtBaseUrl(Q4 不迁移) * - 删除边界(Q6):删激活→自动切默认;删默认→默认转移第一条并激活之;禁删最后一条 */ /** 归一化并校验 HTTP 地址:trim + 去尾斜杠 + 校验 http(s):// 前缀 */ export function normalizeBaseUrl(raw) { const url = String(raw ?? '').trim().replace(/\/+$/, ''); if (!/^https?:\/\//i.test(url) || url.length <= 8) { throw Object.assign(new Error('HTTP 地址格式无效(需以 http:// 或 https:// 开头)'), { code: 'qmt-invalid-url' }); } return url; } /** 生成连接配置英文 id(复用策略拼音 slug;兜底前缀 qmt-conn) */ export function generateQmtConnectionId(name, existingIds = []) { const base = generateStrategyId(name, []); const slug = base && base !== 'strategy' ? base : 'qmt-conn'; return dedupe(slug, existingIds); } /** 读取 QMT 连接配置(list 按 order 排序) */ export function getQmtConnections(scope) { const value = scope?.get() ?? {}; const qc = value.qmtConnections ?? {}; const list = Array.isArray(qc.list) ? qc.list : []; return { list: list.slice().sort((a, b) => (a.order ?? 0) - (b.order ?? 0)), activeId: typeof qc.activeId === 'string' ? qc.activeId : '', defaultId: typeof qc.defaultId === 'string' ? qc.defaultId : '', }; } /** 解析当前生效连接:activeId → defaultId → order 第一条 → null */ export function getActiveQmtConnection(state) { const byId = (id) => (id ? state.list.find((c) => c.id === id) ?? null : null); return byId(state.activeId) ?? byId(state.defaultId) ?? state.list[0] ?? null; } /** * 启动激活连接解析(Q1/Q4):默认 → 上次激活 → 第一条;列表为空回退 cordis qmtBaseUrl。 * 解析结果与 activeId 不一致时回写(保证运行期状态一致)。 * @returns {Promise<{connection: object|null, baseUrl: string|null, source: string}>} */ export async function resolveStartupConnection(scope, config) { const state = getQmtConnections(scope); if (!state.list.length) { const fallback = String(config?.qmtBaseUrl ?? '').trim().replace(/\/+$/, ''); return { connection: null, baseUrl: fallback || null, source: fallback ? 'cordis-fallback' : 'none' }; } const byId = (id) => (id ? state.list.find((c) => c.id === id) ?? null : null); const defaultConn = byId(state.defaultId); const activeConn = byId(state.activeId); const chosen = defaultConn ? { conn: defaultConn, why: 'default' } : activeConn ? { conn: activeConn, why: 'last-active' } : { conn: state.list[0], why: 'first' }; if (chosen.conn.id !== state.activeId) { await activateQmtConnection(scope, chosen.conn.id); } return { connection: chosen.conn, baseUrl: chosen.conn.baseUrl, source: 'settings:' + chosen.why }; } /** 新增连接配置 */ export async function addQmtConnection(scope, { name, baseUrl } = {}) { const trimmedName = String(name ?? '').trim(); if (!trimmedName) throw Object.assign(new Error('连接名称不能为空'), { code: 'qmt-name-required' }); const normalizedUrl = normalizeBaseUrl(baseUrl); const state = getQmtConnections(scope); const id = generateQmtConnectionId(trimmedName, state.list.map((c) => c.id)); const conn = { id, name: trimmedName, baseUrl: normalizedUrl, order: state.list.reduce((max, c) => Math.max(max, c.order ?? 0), -1) + 1, }; const value = scope?.get() ?? {}; await scope.update({ ...value, qmtConnections: { ...state, list: [...state.list, conn] } }); return conn; } /** 编辑连接配置(名称 / 地址);返回更新后的配置对象 */ export async function updateQmtConnection(scope, id, patch = {}) { const state = getQmtConnections(scope); const target = state.list.find((c) => c.id === id); if (!target) throw Object.assign(new Error('连接配置不存在: ' + id), { code: 'qmt-connection-not-found' }); const nextName = patch.name !== undefined ? String(patch.name).trim() : target.name; if (!nextName) throw Object.assign(new Error('连接名称不能为空'), { code: 'qmt-name-required' }); const nextUrl = patch.baseUrl !== undefined ? normalizeBaseUrl(patch.baseUrl) : target.baseUrl; const updated = { ...target, name: nextName, baseUrl: nextUrl }; const value = scope?.get() ?? {}; await scope.update({ ...value, qmtConnections: { ...state, list: state.list.map((c) => (c.id === id ? updated : c)) }, }); return updated; } /** 删除连接配置(含 Q6 删除边界处理);返回删除后的状态与边界信息 */ export async function removeQmtConnection(scope, id) { const state = getQmtConnections(scope); if (!state.list.some((c) => c.id === id)) { throw Object.assign(new Error('连接配置不存在: ' + id), { code: 'qmt-connection-not-found' }); } if (state.list.length <= 1) { throw Object.assign(new Error('不能删除最后一个连接配置'), { code: 'qmt-last-connection' }); } const removedWasActive = state.activeId === id; const removedWasDefault = state.defaultId === id; const list = state.list.filter((c) => c.id !== id); // 删默认 → 默认标记转移到列表第一条(产品约束-005) let defaultId = removedWasDefault ? (list[0]?.id ?? '') : state.defaultId; // 删激活 → 自动切换到默认(若默认仍有效),否则第一条 let activeId = removedWasActive ? (defaultId && list.some((c) => c.id === defaultId) ? defaultId : (list[0]?.id ?? '')) : state.activeId; const value = scope?.get() ?? {}; await scope.update({ ...value, qmtConnections: { list, activeId, defaultId } }); return { list, activeId, defaultId, removedWasActive, removedWasDefault }; } /** 激活连接配置(写 activeId;热切换由 api 层编排 setBaseUrl) */ export async function activateQmtConnection(scope, id) { const state = getQmtConnections(scope); if (!state.list.some((c) => c.id === id)) { throw Object.assign(new Error('连接配置不存在: ' + id), { code: 'qmt-connection-not-found' }); } const value = scope?.get() ?? {}; await scope.update({ ...value, qmtConnections: { ...state, activeId: id } }); return { ...state, activeId: id }; } /** 设为默认配置(写 defaultId) */ export async function setDefaultQmtConnection(scope, id) { const state = getQmtConnections(scope); if (!state.list.some((c) => c.id === id)) { throw Object.assign(new Error('连接配置不存在: ' + id), { code: 'qmt-connection-not-found' }); } const value = scope?.get() ?? {}; await scope.update({ ...value, qmtConnections: { ...state, defaultId: id } }); return { ...state, defaultId: id }; }