@zq-silk/yui 2.7.1 → 2.8.0

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.
Files changed (90) hide show
  1. package/dist/cli/commandCatalog.js +8 -7
  2. package/dist/cli/updateCommand.js +3 -0
  3. package/dist/commands/configCommands.js +1 -0
  4. package/dist/commands/globalRoleCommands.js +42 -35
  5. package/dist/commands/inboxCommands.js +60 -40
  6. package/dist/commands/taskCommands.js +7 -3
  7. package/dist/commands/taskRoleRuntimeStatus.js +2 -0
  8. package/dist/context/runContextPack.js +48 -3
  9. package/dist/context/taskContext.js +36 -12
  10. package/dist/controlPlaneCli.js +46 -12
  11. package/dist/controller/controller.js +5 -0
  12. package/dist/controller/fileSchedulerStoreAdapter.js +9 -4
  13. package/dist/controller/globalInputDelivery.js +17 -5
  14. package/dist/controller/globalRuntimeAttention.js +135 -5
  15. package/dist/controller/runtime.js +4 -0
  16. package/dist/controller/structuredProviderObservation.js +51 -15
  17. package/dist/controller/taskHostTerminalRecovery.js +33 -0
  18. package/dist/executor/fileRoleLaunchPlanner.js +4 -9
  19. package/dist/message/inboxReceipt.js +30 -0
  20. package/dist/message/taskInboxOwnership.js +25 -3
  21. package/dist/models/modelManagement.js +469 -0
  22. package/dist/models/modelManagementConfig.js +46 -0
  23. package/dist/models/modelManagementFiles.js +90 -0
  24. package/dist/models/modelManagementHttp.js +90 -0
  25. package/dist/models/modelManagementTypes.js +1 -0
  26. package/dist/output/boundedRead.js +43 -14
  27. package/dist/runtime/agentEndpointIdentity.js +8 -4
  28. package/dist/runtime/agentEndpointOwnership.js +5 -6
  29. package/dist/runtime/agentHost.js +62 -10
  30. package/dist/runtime/agentHostImplementation.js +18 -0
  31. package/dist/runtime/managedCaller.js +25 -1
  32. package/dist/runtime/managedIdentity.js +1 -1
  33. package/dist/runtime/providerRetry.js +19 -1
  34. package/dist/runtime/sessionCaller.js +53 -0
  35. package/dist/storage/contextRecords.js +28 -0
  36. package/dist/storage/sqliteSchema.js +10 -0
  37. package/dist/storage/sqliteStore.js +9 -5
  38. package/dist/storage/storageVersions.js +1 -1
  39. package/dist/storage/taskStore.js +4 -0
  40. package/dist/task/completionReadiness.js +3 -3
  41. package/dist/web/assets/assetManifest.js +28 -2
  42. package/dist/web/assets/client/app/manage.js +6 -0
  43. package/dist/web/assets/client/app/models.js +20 -0
  44. package/dist/web/assets/client/app/settings.js +41 -9
  45. package/dist/web/assets/client/i18n/en.js +180 -2
  46. package/dist/web/assets/client/i18n/zh-CN.js +178 -2
  47. package/dist/web/assets/client/index.js +8 -0
  48. package/dist/web/assets/client/layout/manageShell.js +94 -0
  49. package/dist/web/assets/client/lib/api.js +13 -1
  50. package/dist/web/assets/client/lib/i18n.js +5 -3
  51. package/dist/web/assets/client/views/dock/conversation.js +231 -80
  52. package/dist/web/assets/client/views/models.js +777 -0
  53. package/dist/web/assets/client/views/skills.js +22 -15
  54. package/dist/web/assets/shared/icons.js +2 -0
  55. package/dist/web/assets/shared/manageNav.js +13 -0
  56. package/dist/web/assets/shell/manage.js +54 -0
  57. package/dist/web/assets/shell/models.js +7 -0
  58. package/dist/web/assets/shell/settings.js +1 -35
  59. package/dist/web/assets/shell/settingsContent.js +30 -0
  60. package/dist/web/assets/shell/sidebar.js +1 -0
  61. package/dist/web/assets/styles/index.js +6 -0
  62. package/dist/web/assets/styles/views/conversation.js +133 -0
  63. package/dist/web/assets/styles/views/manage.js +71 -0
  64. package/dist/web/assets/styles/views/models.js +129 -0
  65. package/dist/web/assets/styles/views/settings.js +28 -14
  66. package/dist/web/assets/styles/views/sidebar.js +1 -0
  67. package/dist/web/webServer.js +59 -9
  68. package/docs/cli-information-contract.md +42 -11
  69. package/docs/cli-information-contract.zh-CN.md +28 -7
  70. package/docs/managed-turn-and-session-runtime.md +47 -1
  71. package/docs/managed-turn-and-session-runtime.zh-CN.md +32 -0
  72. package/docs/release-workflow.md +58 -7
  73. package/docs/release-workflow.zh-CN.md +39 -4
  74. package/docs/storage-baseline.md +6 -1
  75. package/docs/storage-baseline.zh-CN.md +4 -1
  76. package/docs/testing/review-evaluation.md +148 -0
  77. package/docs/testing/review-evaluation.zh-CN.md +117 -0
  78. package/docs/testing/verification-levels.md +6 -2
  79. package/docs/testing/verification-levels.zh-CN.md +3 -0
  80. package/package.json +1 -1
  81. package/skills/yui-leader/VERSION +1 -1
  82. package/skills/yui-leader/references/execution.md +29 -0
  83. package/skills/yui-leader/references/replicated-execution.md +7 -0
  84. package/skills/yui-operator/SKILL.md +5 -1
  85. package/skills/yui-operator/VERSION +1 -1
  86. package/skills/yui-reviewer/SKILL.md +35 -0
  87. package/skills/yui-reviewer/VERSION +1 -1
  88. package/skills/yui-runtime/SKILL.md +53 -7
  89. package/skills/yui-runtime/VERSION +1 -1
  90. package/skills/yui-runtime/references/recovery.md +24 -5
@@ -1,6 +1,170 @@
1
1
  // Simplified Chinese UI copy. Typed against the English catalog, so a key that
2
2
  // does not exist in en.ts is a compile error; a missing key falls back to en.
3
3
  export const ZH_CN = {
4
+ "models.subscriptionAuth": "官方订阅",
5
+ "models.account": "账号",
6
+ "models.providerReadOnlyHelp": "由来源服务管理,请在对应服务中修改配置。",
7
+ "models.readOnlyService": "只读接入,配置由运行中的服务管理。",
8
+ "models.window.5h": "5 小时",
9
+ "models.window.weekly": "每周",
10
+ "models.window.monthly": "每月",
11
+ "manage.nav": "主导航",
12
+ "manage.workbench": "工作台",
13
+ "settings.section.preferences": "偏好设置",
14
+ "settings.section.skills": "Skill",
15
+ "settings.section.server": "Agent 与服务",
16
+ "settings.hint.preferences": "本浏览器的主题、语言与工作台布局。修改立即在此生效,不会发送到服务端。",
17
+ "settings.hint.server": "Yui、Role、Agent 与 Profile 的期望配置。保存面向后续启动,不代表现有 Session 已采用。",
18
+ "models.title": "模型",
19
+ "models.scope": "管理 Yui 读取的 metabridge 与 CLIProxyAPI 连接。保存的配置需重新加载 metabridge 后才生效,本页不会执行推理。",
20
+ "models.section.providers": "接入管理",
21
+ "models.section.catalog": "模型目录",
22
+ "models.section.quota": "订阅额度",
23
+ "models.section.connection": "模型连接",
24
+ "models.openConnection": "前往模型连接",
25
+ "models.connectionGroup.bridge": "metabridge",
26
+ "models.connectionGroup.cpa": "CLIProxyAPI(额度)",
27
+ "models.unsavedDraft": "有未保存的修改",
28
+ "models.applicationTitle": "保存不等于生效",
29
+ "models.column.provider": "Provider",
30
+ "models.column.baseUrl": "基础 URL",
31
+ "models.column.prefix": "前缀",
32
+ "models.column.route": "路由",
33
+ "models.column.state": "状态",
34
+ "models.column.actions": "操作",
35
+ "models.providerCount": "显示 {shown} / {total} 个 Provider",
36
+ "models.noProviderMatches": "没有符合筛选条件的 Provider。",
37
+ "models.searchProviders": "搜索名称、ID、地址或前缀",
38
+ "models.searchCatalog": "搜索已加载的模型 ID",
39
+ "models.filter.route": "按路由筛选",
40
+ "models.filter.state": "按状态筛选",
41
+ "models.filter.source": "按来源筛选",
42
+ "models.filter.all": "全部",
43
+ "models.filter.enabled": "已启用",
44
+ "models.filter.disabled": "已停用",
45
+ "models.filter.readonly": "只读",
46
+ "models.catalogMatches": "匹配 {count}",
47
+ "models.noCatalogMatches": "已加载的模型中没有匹配项;加载更多页后可继续搜索。",
48
+ "models.resetUnknown": "重置时间未知",
49
+ "models.accountIdHelp": "由 Yui 根据上游账号生成的标识,不是账号名。",
50
+ "models.loading": "加载中…",
51
+ "models.loadFailed": "无法读取模型配置:",
52
+ "models.retry": "重试",
53
+ "models.unavailable": "读取成功前无法管理模型配置。",
54
+ "models.unknown": "未知",
55
+ "models.connected": "已配置连接",
56
+ "models.disconnected": "未连接",
57
+ "models.application.unverified": "未验证是否生效",
58
+ "models.applicationHelp": "此处保存只改变期望配置。需按现有部署流程重新加载 metabridge 才会生效;本页不会重启服务、改写 Codex 配置、切换 Session 或验证推理。",
59
+ "models.configurationError": "Bridge 配置问题:",
60
+ "models.unknownBlocked": "之前的一次保存结果未知,不会重发。请先读取当前事实再做修改。",
61
+ "models.readFacts": "读取当前事实",
62
+ "models.reconciledChanged": "已读取当前事实;配置在那次不确定的保存后已变化,请核对后再保存。",
63
+ "models.reconciledUnchanged": "已读取当前事实,暂未看到变化;之前的保存可能仍未落定,请核对后再保存。",
64
+ "models.agentSettings": "Agent 与模型选择仍在设置页中进行,不会自动导入模型。",
65
+ "models.openSettings": "打开设置",
66
+ "models.connection": "连接",
67
+ "models.connectionHelp": "路径指向本地文件;密钥由服务端读取,不会在此显示。断开连接不会停止服务,也不会删除凭据。",
68
+ "models.notConnected": "尚未配置连接,至少需要填写 metabridge URL。",
69
+ "models.connection.bridgeUrl": "metabridge URL(必填)",
70
+ "models.connection.bridgeUrlHelp": "metabridge 管理端点的基础 URL。",
71
+ "models.connection.bridgeConfigFile": "metabridge 配置文件",
72
+ "models.connection.bridgeConfigFileHelp": "可选。填写后可通过此本地文件管理 Provider。",
73
+ "models.connection.cpaUrl": "CLIProxyAPI URL",
74
+ "models.connection.cpaUrlHelp": "可选。与管理密钥文件一起用于刷新额度。",
75
+ "models.connection.managementKeyFile": "CPA 管理密钥文件",
76
+ "models.connection.managementKeyFileHelp": "保存管理密钥的私有文件路径。密钥本身不会返回。",
77
+ "models.connection.catalogKeyFile": "目录密钥文件",
78
+ "models.connection.catalogKeyFileHelp": "可选。与管理密钥相互独立。",
79
+ "models.saveConnection": "保存连接",
80
+ "models.disconnect": "断开连接",
81
+ "models.disconnectConfirm": "断开 Yui 与 metabridge 的连接?服务会继续运行,凭据不会被删除。",
82
+ "models.discard": "放弃修改",
83
+ "models.bridgeUrlRequired": "必须填写 metabridge URL。",
84
+ "models.providers": "Provider",
85
+ "models.providersHelp": "metabridge 中配置的 Provider。官方、订阅以及无前缀的共享路由为只读。",
86
+ "models.noProviders": "尚未配置 Provider。",
87
+ "models.addProvider": "添加 API Key Provider",
88
+ "models.configFileRequired": "需在连接中设置 metabridge 配置文件后才能管理 Provider。",
89
+ "models.newProvider": "新建 Provider",
90
+ "models.editProvider": "编辑",
91
+ "models.removeProvider": "移除",
92
+ "models.removeConfirm": "从 metabridge 移除此 Provider?其文件与凭据会保留。",
93
+ "models.discardProvider": "放弃正在编辑的 Provider?",
94
+ "models.readOnly": "只读",
95
+ "models.noPrefix": "无前缀",
96
+ "models.enabled": "已启用",
97
+ "models.disabled": "已停用",
98
+ "models.credentialStored": "已保存凭据",
99
+ "models.credentialMissing": "无凭据",
100
+ "models.via.direct": "直连",
101
+ "models.via.cpa": "经 CPA",
102
+ "models.via.unknown": "未知路由",
103
+ "models.provider.name": "名称",
104
+ "models.provider.via": "路由",
105
+ "models.provider.viaDirectHelp": "metabridge 使用此 Provider 的 API Key 直接调用。",
106
+ "models.provider.viaCpaHelp": "metabridge 调用 CLIProxyAPI。请使用 CPA 客户端推理密钥,而不是上游账号密钥或管理密钥。",
107
+ "models.provider.baseUrl": "基础 URL",
108
+ "models.provider.prefix": "模型前缀",
109
+ "models.provider.prefixHelp": "显式模型命名空间,以 / 结尾,如 mine/ 对应 mine/model:1–64 个字母、数字、_ 或 -,再加 /。",
110
+ "models.prefixInvalid": "前缀须为 1–64 个字母、数字、_ 或 -,并以 / 结尾,如 mine/。",
111
+ "models.provider.enabled": "启用",
112
+ "models.provider.apiKey": "API Key",
113
+ "models.provider.apiKeyCpa": "CPA 客户端推理密钥",
114
+ "models.provider.apiKeyNewHelp": "必填。仅在保存时发送一次并随即清空,之后不会再显示。",
115
+ "models.provider.apiKeyKeepHelp": "留空则保留已保存的凭据;填写新值会替换它。",
116
+ "models.provider.apiKeyMissingHelp": "尚未保存凭据,填写后即可设置。",
117
+ "models.apiKeyKeep": "保持不变",
118
+ "models.createProvider": "创建 Provider",
119
+ "models.saveProvider": "保存 Provider",
120
+ "models.cancel": "取消",
121
+ "models.providerNewRequired": "新建 Provider 需要名称、基础 URL、前缀和 API Key。",
122
+ "models.providerRequired": "需要名称、基础 URL,以及直连或 CPA 路由。",
123
+ "models.saving": "正在保存…",
124
+ "models.saved": "已保存。重新加载 metabridge 前不会生效。",
125
+ "models.rejected": "未保存,草稿已保留。若配置已变化,请读取当前事实后再保存。",
126
+ "models.rereadChanged": "已读取当前事实;配置已变化。草稿已保留,请对照新事实核对后再保存。",
127
+ "models.rereadUnchanged": "已读取当前事实;配置未变化。草稿已保留。",
128
+ "models.unknownWrite": "保存结果未知,不会重发。",
129
+ "models.catalog": "模型目录",
130
+ "models.catalogHelp": "metabridge 目录缓存。来源取自模型 ID 命名空间,不代表已证实的账号归属。",
131
+ "models.catalogFailed": "目录读取失败:",
132
+ "models.catalogNotQueried": "尚未查询目录。刷新以从 metabridge 读取。",
133
+ "models.catalogEmpty": "此次观测中没有模型。",
134
+ "models.catalogItemsUnknown": "未观测到模型;目录未知,并非为空。",
135
+ "models.catalogCount": "显示 {shown} / {total}",
136
+ "models.catalogChanged": "翻页期间目录已变化,已显示最新的第一页。",
137
+ "models.moreCatalog": "加载更多模型",
138
+ "models.refreshCatalog": "刷新目录",
139
+ "models.quota": "订阅额度",
140
+ "models.quotaHelp": "按账号查看订阅窗口与余额。手动刷新获取最新额度。",
141
+ "models.source": "来源",
142
+ "models.refreshQuota": "刷新额度",
143
+ "models.quotaNeedsCpa": "刷新额度需要 CPA URL 和管理密钥文件。",
144
+ "models.quotaUnavailable": "额度接口不可用,额度未知。这并不证明扩展未安装。",
145
+ "models.quotaFailed": "额度未知:",
146
+ "models.quotaNotQueried": "此来源尚未查询额度。",
147
+ "models.quotaUnsupported": "此来源不提供额度;额度未知,并非为零。",
148
+ "models.quotaEmpty": "此次观测中没有账号。",
149
+ "models.quotaItemsUnknown": "未观测到账号;额度未知,并非为空。",
150
+ "models.noWindows": "未报告额度窗口。",
151
+ "models.remaining": "剩余",
152
+ "models.used": "已用 / 上限",
153
+ "models.resets": "重置于",
154
+ "models.balance": "余额",
155
+ "models.balance.extra-usage-usd": "额外用量余额(美元)",
156
+ "models.balance.monthly-credits": "月度点数",
157
+ "models.balance.purchased-credits": "购买点数",
158
+ "models.balance.free-credits": "免费点数",
159
+ "models.periodEnd": "周期结束",
160
+ "models.status.not-queried": "未查询",
161
+ "models.status.success": "已观测",
162
+ "models.status.partial": "部分",
163
+ "models.status.failed": "失败",
164
+ "models.status.unsupported": "不支持",
165
+ "models.stale": "已过期",
166
+ "models.attempted": "尝试于",
167
+ "models.observed": "观测于",
4
168
  "preview.title": "网页预览",
5
169
  "preview.refresh": "刷新",
6
170
  "preview.loading": "正在检查已登记服务…",
@@ -74,7 +238,6 @@ export const ZH_CN = {
74
238
  "workbench.reviewExecution": "审查执行状态(不代表验收):",
75
239
  "workbench.noEvidence": "尚无已记录的审查或集成检查;可查看 Leader 说明与原始成果。",
76
240
  "workbench.openEvidence": "查看成果、审查、验证与文件",
77
- "settings.scope": "浏览器偏好在此立即生效。服务端保存的是期望配置,不代表现有 Session 已采用。",
78
241
  "settings.browser": "当前浏览器",
79
242
  "settings.more": "加载更多配置组",
80
243
  "settings.groups": "个配置组",
@@ -604,7 +767,7 @@ export const ZH_CN = {
604
767
  "conversation.linkedOmitted": "更多关联输入可在来源记录中查看。",
605
768
  "conversation.stop": "停止本轮",
606
769
  "conversation.receipt": "查询原始回执",
607
- "conversation.older": "上一页历史",
770
+ "conversation.older": "加载更早的消息",
608
771
  "conversation.latest": "最新 / 重连",
609
772
  "conversation.current": "当前会话",
610
773
  "conversation.previous": "历史会话",
@@ -620,6 +783,19 @@ export const ZH_CN = {
620
783
  "conversation.empty": "此页没有公开消息,可继续读取更早的历史。",
621
784
  "conversation.liveUnavailable": "实时回复观测不可用,仅显示原生历史",
622
785
  "conversation.truncated": "长消息仅显示摘要;完整内容请打开原生入口。",
786
+ "conversation.placeholder": "给当前会话发消息…",
787
+ "conversation.more": "更多",
788
+ "conversation.copy": "复制",
789
+ "conversation.copied": "已复制",
790
+ "conversation.jump": "回到最新",
791
+ "conversation.selectModel": "选择模型",
792
+ "conversation.worked": "已处理",
793
+ "conversation.activity.commandExecution": "运行",
794
+ "conversation.activity.fileChange": "修改文件",
795
+ "conversation.activity.mcpToolCall": "调用工具",
796
+ "conversation.activity.dynamicToolCall": "调用工具",
797
+ "conversation.activity.webSearch": "联网搜索",
798
+ "conversation.activity.collabAgentToolCall": "询问其他 Agent",
623
799
  "dock.swap": "与任务详情交换位置",
624
800
  "dock.hide": "隐藏会话面板",
625
801
  "dock.resize": "调整会话面板宽度",
@@ -60,9 +60,14 @@ import { SELECTION_SCRIPT } from "./app/selection.js";
60
60
  import { TASK_VIEW_SCRIPT } from "./app/taskView.js";
61
61
  import { SETTINGS_SCRIPT } from "./app/settings.js";
62
62
  import { SKILLS_SCRIPT } from "./views/skills.js";
63
+ import { MODELS_VIEW_SCRIPT } from "./views/models.js";
64
+ import { MODELS_SCRIPT } from "./app/models.js";
65
+ import { MANAGE_SHELL_SCRIPT } from "./layout/manageShell.js";
66
+ import { MANAGE_SCRIPT } from "./app/manage.js";
63
67
  export { APP_SCRIPT } from "./app/main.js";
64
68
  export const CLIENT_MODULES = Object.freeze({
65
69
  "views/skills": SKILLS_SCRIPT,
70
+ "views/models": MODELS_VIEW_SCRIPT,
66
71
  "lib/dom": DOM_SCRIPT,
67
72
  "lib/format": FORMAT_SCRIPT,
68
73
  "lib/prefs": PREFS_SCRIPT,
@@ -107,8 +112,11 @@ export const CLIENT_MODULES = Object.freeze({
107
112
  "layout/workspace": WORKSPACE_SCRIPT,
108
113
  "layout/router": ROUTER_SCRIPT,
109
114
  "layout/shortcuts": SHORTCUTS_SCRIPT,
115
+ "layout/manageShell": MANAGE_SHELL_SCRIPT,
110
116
  "app/catalog": CATALOG_SCRIPT,
111
117
  "app/taskView": TASK_VIEW_SCRIPT,
112
118
  "app/settings": SETTINGS_SCRIPT,
119
+ "app/manage": MANAGE_SCRIPT,
120
+ "app/models": MODELS_SCRIPT,
113
121
  "app/selection": SELECTION_SCRIPT
114
122
  });
@@ -0,0 +1,94 @@
1
+ import { MANAGE_SECTIONS } from "../../shared/manageNav.js";
2
+ export const MANAGE_SHELL_SCRIPT = String.raw `
3
+ import { createI18n } from "/assets/js/lib/i18n.js";
4
+ const SECTIONS = ${JSON.stringify(MANAGE_SECTIONS)};
5
+ export const i18n = createI18n([document.querySelector("#locale-select"), document.querySelector("#settings-locale")]);
6
+ const pages = new Map();
7
+ const dirty = {};
8
+ let started = false;
9
+ let route;
10
+
11
+ export function manageArea(page, section) {
12
+ return section === "connection" ? "settings" : page;
13
+ }
14
+ export function pageSections(page) {
15
+ return Object.values(SECTIONS).flat().filter(item => item.page === page).map(item => item.section);
16
+ }
17
+ export function readManageRoute(url) {
18
+ const parts = url.pathname.split("/").filter(Boolean);
19
+ const area = parts[0] === "settings" ? "settings" : "models";
20
+ const requested = parts[1] || url.searchParams.get("section");
21
+ const section = (SECTIONS[area].find(item => item.section === requested) ||
22
+ (area === "models" && requested === "connection" ? SECTIONS.settings.at(-1) : SECTIONS[area][0])).section;
23
+ return { page: section === "connection" ? "models" : area, section,
24
+ source: url.searchParams.get("source") === "command-code" ? "command-code" : "factory" };
25
+ }
26
+ export function manageHref(next) {
27
+ return "/" + manageArea(next.page, next.section) + "/" + next.section +
28
+ (next.section === "quota" && next.source === "command-code" ? "?source=command-code" : "");
29
+ }
30
+ function render() {
31
+ if (!started) return;
32
+ const rail = document.querySelector(".manage-rail");
33
+ const area = manageArea(route.page, route.section);
34
+ rail.querySelectorAll("a[data-section]").forEach(link => {
35
+ const active = link.dataset.page === route.page && link.dataset.section === route.section;
36
+ if (active) link.setAttribute("aria-current", "page"); else link.removeAttribute("aria-current");
37
+ if (dirty[link.dataset.page]?.[link.dataset.section]) link.dataset.dirty = "true"; else delete link.dataset.dirty;
38
+ });
39
+ rail.querySelectorAll("a[data-area]").forEach(link => {
40
+ if (link.dataset.area === area) link.setAttribute("aria-current", "true"); else link.removeAttribute("aria-current");
41
+ });
42
+ rail.querySelectorAll("[data-area-group]").forEach(group => {
43
+ if (group.dataset.areaGroup === area) group.dataset.active = "true"; else delete group.dataset.active;
44
+ });
45
+ document.title = i18n.t((route.page === "models" ? "models" : "settings") + ".section." + route.section) +
46
+ " · " + i18n.t(area + ".title") + " · Yui";
47
+ }
48
+ function apply(next) {
49
+ route = next;
50
+ document.querySelector("#models-root").hidden = next.page !== "models";
51
+ document.querySelector("#settings-root").hidden = next.page !== "settings";
52
+ pages.get(next.page).show(next);
53
+ render();
54
+ }
55
+ export function navigateManage(next) {
56
+ const normalized = readManageRoute(new URL(manageHref(next), location.origin));
57
+ if (manageHref(normalized) === location.pathname + location.search) return;
58
+ history.replaceState({ ...history.state, scroll: window.scrollY }, "");
59
+ history.pushState({ scroll: 0 }, "", manageHref(normalized));
60
+ apply(normalized);
61
+ window.scrollTo(0, 0);
62
+ }
63
+ export function createManageShell(options) {
64
+ return {
65
+ set: function () { render(); },
66
+ setDirty: function (flags) { dirty[options.page] = flags; render(); },
67
+ link: section => document.querySelector('.manage-rail a[data-page="' + options.page + '"][data-section="' + section + '"]'),
68
+ render: render
69
+ };
70
+ }
71
+ export function registerManagePage(name, controller) { pages.set(name, controller); }
72
+ export function startManagement() {
73
+ started = true;
74
+ route = readManageRoute(new URL(location.href));
75
+ history.replaceState({ ...history.state, scroll: window.scrollY }, "", manageHref(route));
76
+ history.scrollRestoration = "manual";
77
+ apply(route);
78
+ document.querySelector(".manage-app").hidden = false;
79
+ document.addEventListener("click", event => {
80
+ const link = event.target.closest("a");
81
+ if (!link || link.target || link.hasAttribute("download") || event.defaultPrevented ||
82
+ event.button > 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
83
+ const url = new URL(link.href);
84
+ if (url.origin !== location.origin || !/^\/(models|settings)(\/|$)/.test(url.pathname)) return;
85
+ event.preventDefault();
86
+ navigateManage(readManageRoute(url));
87
+ });
88
+ window.addEventListener("popstate", () => {
89
+ apply(readManageRoute(new URL(location.href)));
90
+ window.scrollTo(0, history.state?.scroll || 0);
91
+ });
92
+ i18n.subscribe(render);
93
+ }
94
+ `;
@@ -137,6 +137,18 @@ export const api = {
137
137
  globalState: function (role) { return requestJson("/api/roles/" + encodeURIComponent(role) + "/control"); },
138
138
  globalControl: function (role, input) {
139
139
  return submitMutation("global/" + role, "/api/roles/" + encodeURIComponent(role) + "/control", input);
140
- }
140
+ },
141
+ // Model management. Configuration writes share one key: the view revision
142
+ // covers the connection and providers together, so an unknown outcome
143
+ // blocks every model write until the page reads current facts again.
144
+ models: function () { return requestJson("/api/models"); },
145
+ modelCatalog: function (cursor) { return requestJson("/api/models/catalog?" + new URLSearchParams({ cursor: cursor || "0" })); },
146
+ modelQuota: function (source) { return requestJson("/api/models/quota?" + new URLSearchParams({ source: source })); },
147
+ saveModelConnection: function (input) { return submitMutation("models/configuration", "/api/models/connection", input); },
148
+ saveModelProvider: function (input) { return submitMutation("models/configuration", "/api/models/provider", input); },
149
+ releaseModelWrites: function () { releaseMutation("models/configuration"); },
150
+ // An explicit refresh is an observation, not a configuration write; a
151
+ // failed or unknown refresh may be requested again by the reader.
152
+ refreshModels: function (input) { return postJson("/api/models/refresh", input); }
141
153
  };
142
154
  `;
@@ -17,7 +17,9 @@ function preferredLocale() {
17
17
  return languages.some(function (language) { return String(language).toLowerCase().startsWith("zh"); }) ? "zh-CN" : "en";
18
18
  }
19
19
 
20
+ // select: one language <select>, a list of them (kept in sync), or none.
20
21
  export function createI18n(select) {
22
+ const selects = (Array.isArray(select) ? select : [select]).filter(Boolean);
21
23
  let locale = preferredLocale();
22
24
  const subscribers = new Set();
23
25
  function t(key, fallback) {
@@ -27,13 +29,13 @@ export function createI18n(select) {
27
29
  function apply() {
28
30
  document.documentElement.lang = locale;
29
31
  document.title = t("app.title");
30
- document.querySelectorAll("[data-i18n]").forEach(function (element) { element.textContent = t(element.dataset.i18n); });
32
+ document.querySelectorAll("[data-i18n]").forEach(function (element) { element.textContent = t(element.dataset.i18n, element.dataset.i18nFallback); });
31
33
  TRANSLATED_ATTRIBUTES.forEach(function (attribute) {
32
34
  document.querySelectorAll("[data-i18n-" + attribute + "]").forEach(function (element) {
33
35
  element.setAttribute(attribute, t(element.getAttribute("data-i18n-" + attribute)));
34
36
  });
35
37
  });
36
- if (select) select.value = locale;
38
+ selects.forEach(function (control) { control.value = locale; });
37
39
  }
38
40
  function setLocale(next) {
39
41
  if (!SUPPORTED_LOCALES.includes(next) || next === locale) return;
@@ -42,7 +44,7 @@ export function createI18n(select) {
42
44
  apply();
43
45
  subscribers.forEach(function (subscriber) { subscriber(locale); });
44
46
  }
45
- if (select) select.addEventListener("change", function () { setLocale(select.value); });
47
+ selects.forEach(function (control) { control.addEventListener("change", function () { setLocale(control.value); }); });
46
48
  apply();
47
49
  return {
48
50
  t: t,