cortico 0.1.0 → 0.1.1

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 (63) hide show
  1. package/README.md +4 -11
  2. package/package.json +1 -3
  3. package/src/bot.ts +1 -1
  4. package/src/core/README.md +0 -4
  5. package/src/core/types.ts +1 -1
  6. package/src/core/util.ts +2 -1
  7. package/src/deploy.ts +8 -6
  8. package/src/providers/README.md +4 -5
  9. package/src/providers/base.ts +1 -1
  10. package/src/providers/configuration.ts +2 -1
  11. package/src/providers/console/settings.ts +8 -10
  12. package/src/providers/llamacpp/console/runtime-panel.ts +73 -32
  13. package/src/providers/llamacpp/console/server.ts +1 -20
  14. package/src/providers/llamacpp/index.ts +0 -2
  15. package/src/providers/llamacpp/options.ts +0 -2
  16. package/src/providers/llamacpp/strings.ts +6 -6
  17. package/src/providers/openai-responses-compat/index.ts +0 -3
  18. package/src/web/README.md +1 -5
  19. package/src/web/client/console-pages/builtins/llm-settings/panel.ts +35 -41
  20. package/src/web/client/console-pages/builtins/llm-settings/strings.ts +4 -4
  21. package/src/web/client/console-pages/host.ts +2 -2
  22. package/src/web/client/features/core/strings.ts +2 -2
  23. package/src/web/client/features/feature.ts +1 -1
  24. package/src/web/client/features/live/index.ts +8 -1
  25. package/src/web/client/features/live/onboarding.ts +4 -1
  26. package/src/web/client/features/live/strings.ts +11 -11
  27. package/src/web/client/features/live/timeline.ts +2 -1
  28. package/src/web/client/features/settings/general.ts +1 -0
  29. package/src/web/client/features/settings/index.ts +1 -0
  30. package/src/web/client/features/worlds/index.ts +1 -0
  31. package/src/web/client/shell/index.ts +27 -47
  32. package/src/web/client/shell/strings.ts +0 -22
  33. package/src/web/client/ui/icons.ts +5 -0
  34. package/src/web/console-pages.ts +1 -1
  35. package/src/web/public/styles.css +21 -8
  36. package/src/web/server.ts +11 -3
  37. package/src/web/shared/client-panel.ts +1 -1
  38. package/src/web/shared/console-protocol.ts +2 -1
  39. package/src/worlds/minecraft/README.md +61 -11
  40. package/src/worlds/minecraft/cell-facts.ts +226 -0
  41. package/src/worlds/minecraft/chests.ts +5 -0
  42. package/src/worlds/minecraft/containers.ts +325 -0
  43. package/src/worlds/minecraft/entity-facts.ts +31 -5
  44. package/src/worlds/minecraft/executor.ts +283 -10348
  45. package/src/worlds/minecraft/inventory.ts +268 -0
  46. package/src/worlds/minecraft/melee.ts +419 -0
  47. package/src/worlds/minecraft/placed-ledger.ts +152 -0
  48. package/src/worlds/minecraft/placement.ts +1038 -0
  49. package/src/worlds/minecraft/receipt.ts +340 -0
  50. package/src/worlds/minecraft/skill-context.ts +358 -0
  51. package/src/worlds/minecraft/skills-build.ts +1169 -0
  52. package/src/worlds/minecraft/skills-container.ts +1343 -0
  53. package/src/worlds/minecraft/skills-craft.ts +333 -0
  54. package/src/worlds/minecraft/skills-dig.ts +624 -0
  55. package/src/worlds/minecraft/skills-gather.ts +1230 -0
  56. package/src/worlds/minecraft/skills-interact.ts +1559 -0
  57. package/src/worlds/minecraft/tools.ts +331 -0
  58. package/src/worlds/minecraft/travel.ts +763 -0
  59. package/src/worlds/minecraft/until.ts +75 -0
  60. package/src/worlds/terminal/world.ts +8 -5
  61. package/src/providers/console/config.ts +0 -21
  62. package/src/providers/llamacpp/config.ts +0 -36
  63. package/src/providers/openai-responses-compat/config.ts +0 -20
@@ -13,7 +13,6 @@ import type {
13
13
  } from '../../../../shared/client-panel.ts';
14
14
  import { pricingEditor, type ModelQuote } from './pricing-panel.ts';
15
15
  import { panel } from './strings.ts';
16
- import { configField, type ConfigGroupEntry } from '../../../features/config/view.ts';
17
16
 
18
17
  /** 每个端点实例的模型与推理配置。 */
19
18
  interface Spec {
@@ -46,7 +45,6 @@ interface Instance {
46
45
  entry: Entry;
47
46
  quotes: ModelQuote[];
48
47
  secretConfigured: 'env' | 'file' | 'none';
49
- config: ConfigGroupEntry[];
50
48
  }
51
49
  interface SettingsState {
52
50
  active: string;
@@ -80,7 +78,7 @@ interface ProbeResult {
80
78
 
81
79
  const DEFAULT_ENDPOINT_PATH = '/responses';
82
80
 
83
- /** 当前端点的模块面板插槽。 */
81
+ /** 模块自有段落挂在这个插槽里,排在连接与模型档之间。 */
84
82
  const MODULE_SLOT = 'instance';
85
83
 
86
84
  /** 开放模式的 effort 格 ↔ spec 的 thinking/reasoningEffort。 */
@@ -159,7 +157,7 @@ export const llmSettingsPanel: ConsolePanel = {
159
157
  let selected = '';
160
158
  /** 模块段落的句柄,重画前先结束上一批。 */
161
159
  let sections: Disposable | null = null;
162
- /** 仅最新一次 load 可以更新 DOM */
160
+ /** 重画代号:慢一步回来的那次 load 不再往 DOM 上贴。 */
163
161
  let generation = 0;
164
162
  const report = ui.msgline();
165
163
  const body = ui.h('div');
@@ -257,37 +255,26 @@ export const llmSettingsPanel: ConsolePanel = {
257
255
  const spec: Spec = structuredClone(entry.spec) ?? { model: '', thinking: open };
258
256
  const saveSpec = () => void patch({ spec });
259
257
 
260
- function configInput(suffix: string, label: string, changed?: (value: unknown) => void | Promise<void>): HTMLElement {
261
- const path = `providers.${current!.name}.${suffix}`;
262
- const { group, values } = current!.config.find(({ group }) => path in group.schema.properties!)!;
263
- const field = configField(ui, group.schema.properties![path], values?.[path], () => {
264
- void (async () => {
265
- const raw = field.read!();
266
- const value = typeof raw === 'string' ? raw.trim() : raw;
267
- try {
268
- await ctx.setConfig(group.id, { [path]: value });
269
- report.textContent = S.saved;
270
- await ctx.refresh();
271
- await changed?.(value);
272
- } catch (error) {
273
- report.textContent = String(error);
274
- }
275
- })();
276
- }, ctx.signal);
277
- (field.node.querySelector('input, select, textarea') ?? field.node).setAttribute('aria-label', label);
278
- return field.node;
279
- }
280
-
281
258
  // ---- 连接 ----
282
259
  const connection = ui.sheet({ title: S.connectionTitle, desc: S.connectionDescription });
283
260
  const options = entry.options ?? {};
284
- const endpointPath =
261
+ let endpointPath =
285
262
  typeof options.endpointPath === 'string' ? options.endpointPath : DEFAULT_ENDPOINT_PATH;
286
263
  const jsonText = (value: unknown) =>
287
264
  value === undefined ? '' : JSON.stringify(value, null, 2);
288
- const baseUrlInput = configInput('baseUrl', S.baseUrl, connectionChanged);
289
- const secretNameInput = configInput('secret', S.secretName, connectionChanged);
290
- (secretNameInput as HTMLInputElement).placeholder = S.secretNamePlaceholder;
265
+ const baseUrlInput = ui.input({
266
+ value: entry.baseUrl,
267
+ cls: 'mono',
268
+ onChange: (value) => void connectionChanged({ baseUrl: value.trim() }),
269
+ });
270
+ baseUrlInput.setAttribute('aria-label', S.baseUrl);
271
+ const secretNameInput = ui.input({
272
+ value: entry.secret ?? '',
273
+ placeholder: S.secretNamePlaceholder,
274
+ cls: 'mono',
275
+ onChange: (value) => void connectionChanged({ secret: value.trim() }),
276
+ });
277
+ secretNameInput.setAttribute('aria-label', S.secretName);
291
278
  connection.body.append(
292
279
  ui.field(S.baseUrl, baseUrlInput),
293
280
  ui.field(S.secretName, secretNameInput),
@@ -307,18 +294,24 @@ export const llmSettingsPanel: ConsolePanel = {
307
294
  connection.body.append(ui.field(S.secretStatus, row));
308
295
  if (!entry.secret) connection.body.append(ui.msgline(S.secretNameFirst));
309
296
  }
310
- connection.body.append(ui.field(S.multimodal, configInput('multimodal', S.multimodal)));
297
+ connection.body.append(
298
+ ui.checkbox(S.multimodal, {
299
+ checked: entry.multimodal === true,
300
+ onChange: (checked) => void patch({ multimodal: checked }),
301
+ }).el,
302
+ );
311
303
  let extraHeaders: HTMLTextAreaElement | null = null;
312
304
  let extraBody: HTMLTextAreaElement | null = null;
313
- let path: HTMLInputElement | null = null;
314
305
  if (open) {
315
- const pathKey = `providers.${current.name}.options.endpointPath`;
316
- const hasPath = current.config.some(({ group }) => pathKey in group.schema.properties!);
317
- path = hasPath ? configInput('options.endpointPath', S.endpointPath) as HTMLInputElement : null;
318
- if (path) {
319
- path.value = endpointPath;
320
- path.placeholder = DEFAULT_ENDPOINT_PATH;
321
- }
306
+ const path = ui.input({
307
+ value: endpointPath,
308
+ cls: 'mono',
309
+ onChange: (value) => {
310
+ endpointPath = value.trim();
311
+ saveOptions();
312
+ },
313
+ });
314
+ path.setAttribute('aria-label', S.endpointPath);
322
315
  extraHeaders = ui.textarea({
323
316
  rows: 3,
324
317
  cls: 'mono',
@@ -335,7 +328,7 @@ export const llmSettingsPanel: ConsolePanel = {
335
328
  extraBody.setAttribute('aria-label', S.extraBody);
336
329
  connection.body.append(
337
330
  ui.section(S.advancedProtocolTitle, S.advancedProtocolDescription),
338
- ...(path ? [ui.field(S.endpointPath, path)] : []),
331
+ ui.field(S.endpointPath, path),
339
332
  ui.field(S.extraHeaders, extraHeaders),
340
333
  ui.field(S.extraBody, extraBody),
341
334
  );
@@ -353,7 +346,7 @@ export const llmSettingsPanel: ConsolePanel = {
353
346
  return parsed;
354
347
  };
355
348
  const edited: Record<string, unknown> = {
356
- endpointPath: path?.value.trim() ?? options.endpointPath,
349
+ endpointPath,
357
350
  extraHeaders: object(extraHeaders!, S.extraHeaders),
358
351
  extraBody: object(extraBody!, S.extraBody),
359
352
  };
@@ -438,7 +431,8 @@ export const llmSettingsPanel: ConsolePanel = {
438
431
  if (!ctx.signal.aborted) renderModelField();
439
432
  }
440
433
  /** 地址或密钥改过就重取目录:换了端点,上一份模型表不作数。 */
441
- async function connectionChanged(): Promise<void> {
434
+ async function connectionChanged(fields: Record<string, unknown>): Promise<void> {
435
+ if (!(await patch(fields))) return;
442
436
  catalog = [];
443
437
  await fetchCatalog();
444
438
  }
@@ -27,12 +27,12 @@ const zh = {
27
27
  extraBody: '附加请求体(JSON 对象)',
28
28
  jsonObjectRequired: (label: string) => `${label} 必须是 JSON 对象`,
29
29
  advancedProtocolTitle: '协议与请求扩展',
30
- advancedProtocolDescription: '',
30
+ advancedProtocolDescription: '针对 OpenAI Responses 兼容端点的高级配置(端点路径与附加 JSON 字段)。',
31
31
  modelFieldLabel: '模型标识',
32
32
  effortFieldLabel: '推理强度',
33
33
  temperatureFieldLabel: '采样温度',
34
34
  specTitle: '模型档',
35
- specDescription: '此端点的每次调用共用这份模型配置;fork 使用创建时的快照。',
35
+ specDescription: '配置此实例调用的模型及推理、温度和 Token 限制等生成参数。',
36
36
  modelName: '明确模型名',
37
37
  fetchModels: '取模型列表',
38
38
  modelsFetched: (count: number) => `取到 ${count} 个模型。`,
@@ -131,12 +131,12 @@ const en: typeof zh = {
131
131
  extraBody: 'Extra body (JSON object)',
132
132
  jsonObjectRequired: (label: string) => `${label} must be a JSON object`,
133
133
  advancedProtocolTitle: 'Protocol & Request Overrides',
134
- advancedProtocolDescription: '',
134
+ advancedProtocolDescription: 'Advanced options for OpenAI Responses compatible endpoints (custom path and extra JSON fields).',
135
135
  modelFieldLabel: 'Model identifier',
136
136
  effortFieldLabel: 'Reasoning effort',
137
137
  temperatureFieldLabel: 'Sampling temperature',
138
138
  specTitle: 'Model',
139
- specDescription: 'Calls on this endpoint share its model configuration; forks use the snapshot from their creation.',
139
+ specDescription: 'Configure model identifier, reasoning effort, temperature and token limits for this instance.',
140
140
  modelName: 'Exact model name',
141
141
  fetchModels: 'Fetch models',
142
142
  modelsFetched: (count: number) => `Fetched ${count} models.`,
@@ -380,11 +380,11 @@ export class ConsolePageHost {
380
380
 
381
381
  /** Persona 页才有工具表页签,且要部署挂着 /api/tool-schemas。 */
382
382
  private toolsTabOf(page: ConsolePageManifest): boolean {
383
- if (page.kind !== 'persona') return false;
383
+ if (page.kind !== 'persona' || page.availability !== 'active') return false;
384
384
  return this.snapshot?.framework?.capabilities?.toolSchemas !== false;
385
385
  }
386
386
 
387
- /** 工具表包含主循环与装配层补充的工具声明。 */
387
+ /** 工具表是整份装配结果,不按页筛:模型看见的那一张表就是这一张。 */
388
388
  private async showTools(pageId: string, gen: number): Promise<void> {
389
389
  const { slot } = this.ensurePanes();
390
390
  const lifecycle = new Lifecycle(this.deps.onError);
@@ -12,7 +12,7 @@ const zh = {
12
12
  subData: '数据',
13
13
  subConfig: '配置',
14
14
  dataTitle: 'Core 的数据',
15
- dataDesc: '',
15
+ dataDesc: '事件库、session、运行日志、用量流水这些 Core 自己攒下的存储。',
16
16
  configTitle: 'Core 配置',
17
17
  configDesc: '修改自动保存到 config.json。标有重启要求的配置在重启后生效,其余立即生效。',
18
18
  configEmpty: 'Core 没有可调配置。',
@@ -89,7 +89,7 @@ const en: typeof zh = {
89
89
  subData: 'Data',
90
90
  subConfig: 'Config',
91
91
  dataTitle: 'Core data',
92
- dataDesc: '',
92
+ dataDesc: 'Storage Core accumulates on its own: the event store, the session, run logs and the usage ledger.',
93
93
  configTitle: 'Core config',
94
94
  configDesc: 'Changes save to config.json automatically. Settings marked for restart apply after restarting; the rest apply immediately.',
95
95
  configEmpty: 'Core has no tunable settings.',
@@ -57,7 +57,7 @@ export type FrameworkFeature = {
57
57
  readonly label: string;
58
58
  /** 左栏图标。 World 那一页不走此字段。 */
59
59
  readonly icon?: ConsoleIconName;
60
- /** 状态灯表中的键;缺省时不声明状态灯。 */
60
+ /** 这一行那盏灯在灯表里的键。省略就不点灯。 */
61
61
  readonly lampId?: string;
62
62
  /** 列出的 capability 任一已挂载即显示导航项;省略或为空时始终显示。 */
63
63
  readonly needsAny?: readonly string[];
@@ -174,6 +174,7 @@ function mountLive(ctx: FeatureContext, env: SocketEnv): Disposable | void {
174
174
  view.append(timeline.el, composer.el);
175
175
  timeline.rebuild([], { empty: S.emptyConnecting });
176
176
 
177
+ // 没有可用端点时说清楚要去哪儿,而不是让操作员发出一条得不到回复的消息。
177
178
  let providerReady = false;
178
179
  ctx.lifecycle.own(subscribeLamps(ctx.root.ownerDocument, (lamps) => {
179
180
  const lamp = lamps[PROVIDERS_LAMP_ID]?.[0];
@@ -184,12 +185,17 @@ function mountLive(ctx: FeatureContext, env: SocketEnv): Disposable | void {
184
185
 
185
186
  const resumeRun = (): Promise<unknown> => post('/api/run/resume', {}, { signal: ctx.signal });
186
187
 
188
+ /**
189
+ * 开场引导认部署目录里那个一次性标记(`.onboarding`,自建部署时写入)。session 与事件游标
190
+ * 都当不了判据:全新部署起来就有一条系统前缀和一条 session 开场事件,而上下文交接后 session
191
+ * 反倒是空的。
192
+ */
187
193
  let onboarding: OnboardingView | null = null;
188
194
  /** 本次挂载里已经收过一次;销标记的请求在路上时状态帧还会报 pending。 */
189
195
  let dismissed = false;
190
196
  const dismissOnboarding = (): void => {
191
197
  dismissed = true;
192
- void post('/api/onboarding/dismiss', {}, { signal: ctx.signal }).catch(() => { /* 删除失败时标记仍在,下次挂载仍显示引导。 */ });
198
+ void post('/api/onboarding/dismiss', {}, { signal: ctx.signal }).catch(() => { /* 下次打开再收 */ });
193
199
  syncOnboarding();
194
200
  };
195
201
  const syncOnboarding = (): void => {
@@ -212,6 +218,7 @@ function mountLive(ctx: FeatureContext, env: SocketEnv): Disposable | void {
212
218
  });
213
219
  onboarding.setProvider(providerReady);
214
220
  timeline.setHeader(onboarding.el);
221
+ // 引导期间系统前缀那张卡先收起来:这一页此刻要说的是怎么把 bot 配起来。
215
222
  timeline.setHideSystem(true);
216
223
  timeline.rebuild(state.messages, { head: state.head });
217
224
  return;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 开场引导的配置状态与操作入口。
2
+ * 新部署的开场引导:四条 Cortico 署名的气泡,用 assistant 直接输出那套气泡样式。
3
3
  * 端点状态由终端页从 providers 灯推进来,已启用的 World 在挂载时读一次接口。
4
4
  * 是否显示由终端页判定,这里只画。
5
5
  */
@@ -60,6 +60,7 @@ export function createOnboarding(deps: OnboardingDeps): OnboardingView {
60
60
  const box = ui.h('div', 'monolog');
61
61
  box.appendChild(ui.h('div', 'monolog-body', line));
62
62
  const state = ui.h('div', 'ob-state hidden');
63
+ // 外观取子页签那颗按钮(`.seg`),`ob-btn` 只挂本页的微调。
63
64
  const button = ui.h('button', action?.accent ? 'seg active ob-btn ob-go' : 'seg active ob-btn');
64
65
  button.type = 'button';
65
66
  if (action?.icon) button.appendChild(icon(doc, action.icon));
@@ -87,6 +88,7 @@ export function createOnboarding(deps: OnboardingDeps): OnboardingView {
87
88
  bubble(S.obPrompts, { label: S.obGoEdit, onClick: () => deps.go(['prompts']) });
88
89
 
89
90
  const startLabel = S.obStart;
91
+ // 最后那颗按的是「开始跑」,图标与左下角运行控制里的继续是同一个。
90
92
  const ready = bubble(S.obReady, {
91
93
  label: startLabel,
92
94
  icon: 'play',
@@ -106,6 +108,7 @@ export function createOnboarding(deps: OnboardingDeps): OnboardingView {
106
108
  el,
107
109
  setProvider(available) {
108
110
  provider.setState(available ? S.obProviderReady : S.obProviderNone, available ? 'ok' : 'bad');
111
+ // 最后一条只在还缺端点时说话:配好了就只剩那颗按钮。
109
112
  ready.setState(available ? null : S.obProviderNone, 'bad');
110
113
  ready.button.disabled = !available;
111
114
  },
@@ -111,14 +111,14 @@ const zh = {
111
111
 
112
112
  // onboarding.ts
113
113
  obWho: 'Cortico',
114
- obWelcome: '欢迎使用 Cortico。',
115
- obProvider: '配置模型提供商后即可开始对话。',
114
+ obWelcome: '欢迎使用 Cortico!现在,让我们开始部署你的第一个 Cortico Bot。',
115
+ obProvider: '首先,请配置模型提供商:',
116
116
  obProviderNone: '尚未配置可用的模型提供商',
117
117
  obProviderReady: '模型提供商已配置',
118
- obWorlds: '按需启用 World,也可以仅使用终端对话。',
119
- obWorldsState: (labels: readonly string[]) => `已启用 ${labels.length} 个 World:${labels.join('、')}`,
120
- obPrompts: '系统提示词定义人格描述、行为规范和语言风格,可按需修改。',
121
- obReady: '配置好后,可以邀请 bot 开口。',
118
+ obWorlds: '接下来,请启用并配置你的 Bot 接入的外部环境模组(Cortico World)。也可以先仅启用终端对话。',
119
+ obWorldsState: (labels: readonly string[]) => `当前已经启用了 ${labels.length} 个外部环境:${labels.join('、')}`,
120
+ obPrompts: '最后,你可以在这里方便地编辑系统提示词,来提供人格描述、行为规范、语言风格等定制化内容!',
121
+ obReady: '准备就绪!',
122
122
  obGoConfigure: '前往配置',
123
123
  obGoEdit: '开始编辑',
124
124
  obStart: '打个招呼?',
@@ -235,15 +235,15 @@ const en: typeof zh = {
235
235
 
236
236
  // onboarding.ts
237
237
  obWho: 'Cortico',
238
- obWelcome: "Welcome to Cortico.",
239
- obProvider: 'Configure a model provider to start chatting.',
238
+ obWelcome: "Welcome to Cortico! Let's set up your first Cortico Bot.",
239
+ obProvider: 'First, configure a model provider:',
240
240
  obProviderNone: 'No usable model provider yet',
241
241
  obProviderReady: 'Model provider configured',
242
- obWorlds: 'Enable Worlds as needed, or use terminal chat alone.',
242
+ obWorlds: 'Next, enable and configure the external environments your bot reaches (Cortico Worlds). Terminal chat alone is a fine start.',
243
243
  obWorldsState: (labels: readonly string[]) =>
244
244
  `Currently ${labels.length} external environment${labels.length === 1 ? '' : 's'} enabled: ${labels.join(', ')}`,
245
- obPrompts: 'You can edit the system prompt to set personality, behavior and language style.',
246
- obReady: 'Once configured, you can invite the bot to speak.',
245
+ obPrompts: 'Finally, the system prompt is edited here: who it is, how it behaves, how it talks.',
246
+ obReady: 'Ready to go!',
247
247
  obGoConfigure: 'Configure',
248
248
  obGoEdit: 'Edit',
249
249
  obStart: 'Say hello?',
@@ -100,7 +100,7 @@ export interface TimelineView {
100
100
  append(m: ContextRecord, index: number, live: boolean): void;
101
101
  /** 说话人展示名。头像图片加载不到时,ASSISTANT 组左栏的占位圆里印它的首字;下一次画到组时生效。 */
102
102
  setSpeaker(name: string): void;
103
- /** 是否隐藏系统前缀,下一次重画生效。 */
103
+ /** 开场引导期间把系统前缀那张卡收起来,下一次重画生效。 */
104
104
  setHideSystem(hide: boolean): void;
105
105
  /** 在滚动区顶部挂一块外部内容,随时间线一起滚;传 null 取下。重画不动它。 */
106
106
  setHeader(node: HTMLElement | null): void;
@@ -131,6 +131,7 @@ export function createTimeline(deps: TimelineDeps): TimelineView {
131
131
  let turn: Turn | null = null;
132
132
  /** 头像占位圆里的字。 */
133
133
  let speakerInitial = 'B';
134
+ /** 收起系统前缀卡。 */
134
135
  let hideSystem = false;
135
136
  /** 挂在滚动区顶部的外部内容(开场引导)。 */
136
137
  let header: HTMLElement | null = null;
@@ -5,6 +5,7 @@ import { S } from './strings.ts';
5
5
  export function mountGeneral(ctx: FeatureContext): void {
6
6
  const { ui, root, signal } = ctx;
7
7
  const win = root.ownerDocument.defaultView!;
8
+ // 同页别处都是档案卡:标题与说明的字体从卡片来,这里自己写 h3/p 会与它们对不齐。
8
9
  const sheet = ui.sheet({ title: S.language, en: 'language', desc: S.languageDesc });
9
10
  const group = ui.h('div', 'rowbar');
10
11
  group.setAttribute('role', 'group');
@@ -73,6 +73,7 @@ export function mountSettings(ctx: FeatureContext): void {
73
73
  if (visible[0]) select(visible[0].id);
74
74
  }
75
75
 
76
+ // 入口是底栏那颗齿轮,不占左栏一行;`hidden` 只保留路由。
76
77
  export const settingsFeature: FrameworkFeature = {
77
78
  route: 'settings',
78
79
  label: S.navLabel,
@@ -179,6 +179,7 @@ export function mountWorlds(ctx: FeatureContext): void {
179
179
  );
180
180
  if (ctx.signal.aborted) return;
181
181
  setMsg(out?.result || (wantEnabled ? S.activated : S.deactivated));
182
+ // 左栏重排失败只是导航旧了一拍,激活本身已经成功,不进上面那行的失败文案。
182
183
  void ctx.refreshNav?.().catch(ctx.onError);
183
184
  await load();
184
185
  } catch (err) {
@@ -117,6 +117,7 @@ export function createShell(deps: ShellDeps): ConsoleShell {
117
117
  const el = ui.h('div');
118
118
  el.id = 'rail';
119
119
 
120
+ // 左上角只有框架字标。这个 bot 叫什么写在底栏头像旁边——那才是这一台的名字。
120
121
  const brand = ui.h('div', 'brand');
121
122
  brand.setAttribute('role', 'img');
122
123
  brand.setAttribute('aria-label', FRAMEWORK_NAME);
@@ -127,8 +128,14 @@ export function createShell(deps: ShellDeps): ConsoleShell {
127
128
 
128
129
  const foot = ui.h('div', 'railfoot');
129
130
  const avatar = createAvatarControl({ doc, ui, signal, onError });
130
- const botName = ui.h('button', 'rail-name', DEFAULT_BRAND);
131
+ const botName = ui.h('button', 'rail-name');
131
132
  botName.type = 'button';
133
+ // 名字自己是按钮(点了就地改名);铅笔只在悬停时露出来,不占额外宽度。
134
+ const botNameText = ui.h('span', 'rail-name-text', DEFAULT_BRAND);
135
+ const botNameEdit = ui.h('span', 'rail-name-edit');
136
+ botNameEdit.setAttribute('aria-hidden', 'true');
137
+ botNameEdit.appendChild(icon(doc, 'pencil'));
138
+ botName.append(botNameText, botNameEdit);
132
139
  const who = ui.h('div', 'rail-who');
133
140
  who.append(avatar.el, botName);
134
141
  const footActions = ui.h('div', 'rail-actions');
@@ -137,16 +144,12 @@ export function createShell(deps: ShellDeps): ConsoleShell {
137
144
  const shutdownButton = ui.h('button', 'rail-action rail-shutdown');
138
145
  shutdownButton.type = 'button';
139
146
  shutdownButton.appendChild(icon(doc, 'power'));
140
- // 重启 = 同一套收尾 + 退出前落重启标志,启动器循环把进程拉起来。
141
- const restartButton = ui.h('button', 'rail-action rail-restart');
142
- restartButton.type = 'button';
143
- restartButton.appendChild(icon(doc, 'refresh'));
144
147
  const settingsButton = ui.h('button', 'rail-action');
145
148
  settingsButton.type = 'button';
146
149
  settingsButton.setAttribute('aria-label', S.settingsAria);
147
150
  settingsButton.title = S.settingsTitle;
148
151
  settingsButton.appendChild(icon(doc, 'settings'));
149
- footActions.append(runButton, settingsButton, restartButton, shutdownButton);
152
+ footActions.append(runButton, settingsButton, shutdownButton);
150
153
  foot.append(who, footActions);
151
154
 
152
155
  let paused = false;
@@ -161,7 +164,7 @@ export function createShell(deps: ShellDeps): ConsoleShell {
161
164
  };
162
165
  renderRun();
163
166
 
164
- /** 关机和重启共用请求锁,等待请求完成后释放。 */
167
+ /** 关机请求的锁,等待请求完成后释放。 */
165
168
  let shuttingDown = false;
166
169
  const renderPower = (): void => {
167
170
  const canShutdown = capabilities.shutdown === true;
@@ -169,45 +172,23 @@ export function createShell(deps: ShellDeps): ConsoleShell {
169
172
  shutdownButton.disabled = !canShutdown || shuttingDown;
170
173
  shutdownButton.setAttribute('aria-label', S.shutdownAria);
171
174
  shutdownButton.title = shuttingDown ? S.finishing : S.shutdownTitle;
172
- const canRestart = capabilities.restart === true;
173
- restartButton.hidden = !canRestart;
174
- restartButton.disabled = !canRestart || shuttingDown;
175
- restartButton.setAttribute('aria-label', S.restartAria);
176
- restartButton.title = shuttingDown
177
- ? S.finishing
178
- : capabilities.supervised === true
179
- ? S.restartTitleSupervised
180
- : S.restartTitleUnsupervised;
181
175
  };
182
176
  renderPower();
183
177
 
184
- const powerAction = async (kind: 'shutdown' | 'restart'): Promise<void> => {
178
+ const powerAction = async (): Promise<void> => {
185
179
  if (shuttingDown) return;
186
- if (kind === 'shutdown' ? capabilities.shutdown !== true : capabilities.restart !== true) return;
187
- const supervised = capabilities.supervised === true;
188
- const first = await ui.confirm(kind === 'shutdown'
189
- ? {
190
- title: S.confirmShutdownTitle,
191
- body: S.shutdownBody,
192
- danger: true,
193
- }
194
- : {
195
- title: S.confirmRestartTitle,
196
- body: supervised ? S.restartSupervisedNote : S.restartUnsupervisedNote,
197
- danger: true,
198
- });
180
+ if (capabilities.shutdown !== true) return;
181
+ const first = await ui.confirm({
182
+ title: S.confirmShutdownTitle,
183
+ body: S.shutdownBody,
184
+ danger: true,
185
+ });
199
186
  if (!first || signal.aborted) return;
200
- const second = await ui.confirm(kind === 'shutdown'
201
- ? {
202
- title: S.confirmShutdownAgainTitle,
203
- body: S.confirmShutdownAgainBody,
204
- danger: true,
205
- }
206
- : {
207
- title: S.confirmRestartAgainTitle,
208
- body: supervised ? S.confirmRestartAgainSupervised : S.confirmRestartAgainUnsupervised,
209
- danger: true,
210
- });
187
+ const second = await ui.confirm({
188
+ title: S.confirmShutdownAgainTitle,
189
+ body: S.confirmShutdownAgainBody,
190
+ danger: true,
191
+ });
211
192
  if (!second || signal.aborted) return;
212
193
  shuttingDown = true;
213
194
  renderPower();
@@ -217,7 +198,7 @@ export function createShell(deps: ShellDeps): ConsoleShell {
217
198
  ok?: boolean; localComplete?: boolean; result?: string; error?: string;
218
199
  steps?: Array<{ label: string; ok: boolean; ms: number; detail?: string }>;
219
200
  externalChecks?: Array<{ status: 'verified-ended' | 'still-live' | 'unknown' }>;
220
- }>(kind === 'shutdown' ? '/api/run/shutdown' : '/api/run/restart', undefined, { signal });
201
+ }>('/api/run/shutdown', undefined, { signal });
221
202
  if (out?.error) throw new Error(out.error);
222
203
  const steps = out?.steps ?? [];
223
204
  const localComplete = out?.localComplete ?? steps.every((step) => step.ok);
@@ -225,13 +206,12 @@ export function createShell(deps: ShellDeps): ConsoleShell {
225
206
  .some((check) => check.status !== 'verified-ended');
226
207
  const lines = steps.map((s) =>
227
208
  `${s.ok ? '✓' : '✗'} ${s.label} · ${(s.ms / 1000).toFixed(1)}s${s.ok ? '' : ` — ${s.detail ?? S.stepIncomplete}`}`);
228
- const done = kind === 'shutdown' ? S.doneShutdown : supervised ? S.doneRestartSupervised : S.doneRestart;
229
209
  void ui.confirm({
230
210
  title: !localComplete
231
211
  ? S.resultLocalIncomplete
232
212
  : externalUnverified
233
213
  ? S.resultExternalUnverified
234
- : out?.ok === false ? S.resultUnverified : done,
214
+ : out?.ok === false ? S.resultUnverified : S.doneShutdown,
235
215
  body: [out?.result ?? S.resultDefault, '', ...lines].join('\n'),
236
216
  });
237
217
  } catch (err) {
@@ -246,8 +226,7 @@ export function createShell(deps: ShellDeps): ConsoleShell {
246
226
  renderPower();
247
227
  }
248
228
  };
249
- shutdownButton.addEventListener('click', () => { void powerAction('shutdown'); }, { signal });
250
- restartButton.addEventListener('click', () => { void powerAction('restart'); }, { signal });
229
+ shutdownButton.addEventListener('click', () => { void powerAction(); }, { signal });
251
230
 
252
231
  runButton.addEventListener('click', () => {
253
232
  if (runPending || capabilities.run !== true) return;
@@ -456,7 +435,8 @@ export function createShell(deps: ShellDeps): ConsoleShell {
456
435
  const setBrand = (name: string | null | undefined): void => {
457
436
  const shown = typeof name === 'string' && name.trim() !== '' ? name.trim() : DEFAULT_BRAND;
458
437
  shownName = shown;
459
- botName.textContent = shown;
438
+ botNameText.textContent = shown;
439
+ // 悬停说的是这一下能做什么;全名在改名框里看得到。
460
440
  botName.title = S.renameTitle;
461
441
  avatar.setLabel(shown);
462
442
  doc.title = S.docTitle(shown);
@@ -13,24 +13,13 @@ const zh = {
13
13
  settingsTitle: '设置',
14
14
  finishing: '正在关闭…',
15
15
  shutdownTitle: '关机',
16
- restartAria: '重启',
17
- restartTitleSupervised: '重启',
18
- restartTitleUnsupervised: '退出后需手动启动',
19
16
  shutdownBody: '进程将退出,控制台连接将断开。',
20
17
  confirmShutdownTitle: '⚠ 关机',
21
- confirmRestartTitle: '⚠ 重启',
22
- restartSupervisedNote: '进程将重启。',
23
- restartUnsupervisedNote: '进程将退出,需要手动重新启动。',
24
18
  confirmShutdownAgainTitle: '⚠⚠ 确认关机?',
25
19
  confirmShutdownAgainBody: '停止运行并退出进程?',
26
- confirmRestartAgainTitle: '⚠⚠ 确认重启?',
27
- confirmRestartAgainSupervised: '退出并重新启动进程?',
28
- confirmRestartAgainUnsupervised: '退出进程?退出后需手动启动。',
29
20
  finishingToast: '正在关闭…',
30
21
  stepIncomplete: '未完成',
31
22
  doneShutdown: '已关机',
32
- doneRestartSupervised: '已退出,等待重新启动',
33
- doneRestart: '已退出',
34
23
  resultLocalIncomplete: '本地关闭步骤未全部完成',
35
24
  resultExternalUnverified: '本地已关机,外部状态未确认',
36
25
  resultUnverified: '关闭步骤已执行,状态未确认',
@@ -56,24 +45,13 @@ const en: typeof zh = {
56
45
  settingsTitle: 'Settings',
57
46
  finishing: 'Shutting down…',
58
47
  shutdownTitle: 'Shut down',
59
- restartAria: 'Restart',
60
- restartTitleSupervised: 'Restart',
61
- restartTitleUnsupervised: 'Manual startup required after exit',
62
48
  shutdownBody: 'The process will exit and disconnect the console.',
63
49
  confirmShutdownTitle: '⚠ Shut down',
64
- confirmRestartTitle: '⚠ Restart',
65
- restartSupervisedNote: 'The process will restart.',
66
- restartUnsupervisedNote: 'The process will exit and must be restarted manually.',
67
50
  confirmShutdownAgainTitle: '⚠⚠ Confirm shutdown?',
68
51
  confirmShutdownAgainBody: 'Stop running and exit the process?',
69
- confirmRestartAgainTitle: '⚠⚠ Confirm restart?',
70
- confirmRestartAgainSupervised: 'Exit and restart the process?',
71
- confirmRestartAgainUnsupervised: 'Exit the process? You must start it manually afterwards.',
72
52
  finishingToast: 'Shutting down…',
73
53
  stepIncomplete: 'incomplete',
74
54
  doneShutdown: 'Shut down',
75
- doneRestartSupervised: 'Exited; waiting to restart',
76
- doneRestart: 'Exited',
77
55
  resultLocalIncomplete: 'Local shutdown steps incomplete',
78
56
  resultExternalUnverified: 'Shut down locally; external state unverified',
79
57
  resultUnverified: 'Shutdown steps executed; state unverified',
@@ -17,6 +17,7 @@ export type ConsoleIconName =
17
17
  | 'text'
18
18
  | 'eye'
19
19
  | 'eye-off'
20
+ | 'pencil'
20
21
  | 'refresh';
21
22
 
22
23
  type Shape = readonly [tag: 'path' | 'circle' | 'rect' | 'line', attrs: Readonly<Record<string, string>>];
@@ -93,6 +94,10 @@ const SHAPES: Readonly<Record<ConsoleIconName, readonly Shape[]>> = {
93
94
  ['path', { d: 'M6.6 6.6C3.8 8.5 2 12 2 12s3.5 7 10 7c1.6 0 3-.4 4.3-1' }],
94
95
  ['path', { d: 'M9.9 9.9a3 3 0 0 0 4.2 4.2' }],
95
96
  ],
97
+ pencil: [
98
+ ['path', { d: 'M4 20h4.2L19.4 8.8a2.3 2.3 0 0 0-3.2-3.2L5 16.8V20Z' }],
99
+ ['path', { d: 'm12.6 6.5 4.9 4.9' }],
100
+ ],
96
101
  refresh: [
97
102
  ['path', { d: 'M21 12a9 9 0 1 1-2.6-6.4' }],
98
103
  ['path', { d: 'M21 3v6h-6' }],
@@ -318,7 +318,7 @@ export class ConsolePageRegistry {
318
318
  ): Promise<{ ok: true; value: unknown } | { ok: false; failure: InvokeFailure }> {
319
319
  const found = await this.resolvePanel(pageId, panelId, language);
320
320
  if (!found.ok) return found;
321
- // GET 只允许 getMethods 声明的方法。
321
+ // GET 不过写请求的同源闸门,任何站点凭 <img src> 就能发出;只放行面板点名的方法。
322
322
  if (transport === 'get' && !(found.panel.getMethods ?? []).includes(method)) {
323
323
  return {
324
324
  ok: false,