Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ WebUI 按 schema 渲染表单。owner 是 `core`、`persona`、`world:<id>` 或

| 键 | 作用 |
|---|---|
| `x-hot` | `false` 时前端标「重启生效」;Core 组每一项都是热改 |
| `x-hot` | `false`:改动要等所属实例重新构造才生效,前端标「重启生效」。省略或 `true`:读取方每次现读,保存后立即生效。Core 组每一项都是热改;World 的读法见 [worlds.md](worlds.md) |
| `x-scale`、`x-suffix` | 显示倍率与单位 |
| `x-options` | 下拉项 |
| `x-path` | 用本机路径选择器填 |
Expand Down
4 changes: 2 additions & 2 deletions docs/webui.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ bot 的展示名。面板、配置组与提示词文档由贡献方声明;
数据接口)、`stream`(面板的推送通道)。World 通过 `World.webUi()` 声明,Persona 通过
`Persona.webUi()` 声明。面板方法只接受 POST;`getMethods` 点名的方法才接受 GET(轮询读、
`<audio src>` 这类只能带 URL 的场合)。存储项由装配层按来源盖上归属 `owner`(`core` / `persona` /
`memory` / `world:<id>`),`/api/storage` 原样回它。
`memory` / `world:<id>`),`/api/storage` 原样回它;不同归属可以有同名 key,manifest 的 `storageParts` 与清除接口都按归属和 key 一起认。

面板带 `slot` 就没有自己的页签,由同页某块面板调 `ctx.mountSlot(slot, 容器, 作用域)` 挂进去,
同一插槽的多块按声明顺序排,作用域进子面板的 `ctx.scope`;返回的句柄结束这一批。`llm:*` 页的面板
Expand Down Expand Up @@ -105,7 +105,7 @@ bot 的展示名。面板、配置组与提示词文档由贡献方声明;
| `GET /api/config/options/:kind` | schema 里 `x-options` 为 `kind` 的配置项的当前候选值 |
| `GET /api/blob?handle=` | 附件字节 |
| `GET /api/worlds`、`POST /api/worlds/activation` | World 清单;激活或停用,体 `{ id, enabled }` |
| `POST /api/storage/clear?key=` | 清除一个存储部分;`key` 取自各页 manifest 的 `storageKeys`,Core 自己的对话历史是 `session` |
| `POST /api/storage/clear?owner=&key=` | 清除一个存储部分;`owner` 与 `key` 取自各页 manifest 的 `storageParts`,Core 自己的对话历史是 `core` 的 `session`。省略 `owner` 时 `key` 须只有一个归属,有多个时回 409 |
| `GET /api/theme`、`POST /api/theme` | 部署的配色;GET 回 `{ defaultScheme, theme }`,POST 体是 `StoredTheme`(`src/web/shared/theme.ts`),回 `{ ok, theme }` |
| `GET /api/avatar`、`POST /api/avatar` | bot 头像 PNG,没有时 404;POST 体是 `image/png` 原始字节,不是 PNG 时回 400 |
| `GET /api/usage` | 用量聚合;查询参数 `bucket`(`auto`/`minute`/`hour`/`day`/`week`/`month`)、`currency`、`basis`(`marginal`/`equivalent`)、`from`、`to`、`days`(不小于 1 的整数) |
Expand Down
9 changes: 9 additions & 0 deletions docs/worlds.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ World 应通过事件报告服务器起停、存档切换、连接变化等状
`configOptions(kind, language)` 每次传入(见 [webui.md](webui.md))。WebUI 里的 World 名取
`webUi(language).label`,没有时取定义的 `label`,两者都按请求现读。

`ctx.cfg` 是部署配置里 `worlds.<id>` 段的共享引用,WebUI 改配置时原地写入这个对象。World 用到时现读
`ctx.cfg.<键>`,改动立即生效;构造时存进字段、或用来建立连接和启动进程的值,要等实例重新构造(停用后
再启用、World 重启、bot 重启)才生效。这类项在 schema 里标 `x-hot: false`,设置页显示「重启生效」;没标的项
按立即生效显示。

`ctx.dataDir` 是部署的数据目录,Core 与这个部署的全部 World 共用,同名文件会互相覆盖。World 的文件名与
`webUi().storage` 的 key 都以 World id 开头,仓内 World 写作 `minecraft-chests.json` 与 `minecraft-chests`。
Comment on lines +88 to +89

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这句话不成立:仓内 QQ World 的 webUi().storage key 是 vision,落盘目录是 data/vision/,都不以 World id qq 开头。

AGENTS.md §4 Accuracy after editing:改完之后保留的行为、条件和边界必须仍然准确、具体。

docs/worlds.md 第 88–89 行:「World 的文件名与 webUi().storage 的 key 都以 World id 开头,仓内 World 写作 minecraft-chests.json 与 minecraft-chests。」PR 说明里同一句:「ctx.dataDir 部署共用,文件名与 storage key 以 World id 开头。」

请按清单核对该句:对照现有 World 的文件名与 storage key,使它与代码一致。

— Cortico PR Gate via Cursor Cloud Agent

装配层给每个存储项盖上归属 `world:<id>`,数据页和清除接口按归属与 key 一起区分。

仓内定义列在 `src/worlds/index.ts`,由 `loadBuiltinWorlds(ids?)` 按 id 动态导入 `src/worlds/<id>/definition.ts`;
不给 `ids` 时带上全部(`BUILTIN_WORLD_IDS`),有不认识的 id 时抛错。启动器带上全部仓内 World,与扩展定义
合并后通过 `withWorlds()` 交给 bot 定义。
Expand Down
8 changes: 4 additions & 4 deletions src/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export interface WebUiContribution {
/** 装配层向 bot WebUI 声明函数提供的共享数据。 */
export interface WebUiPageBuildContext {
/** 与 /api/storage 共用同一批对象的存储清单,文案使用请求语言。 */
storage: readonly StoragePart[];
storage: readonly OwnedStoragePart[];
/** 发起这次请求的浏览器的界面语言。 */
language: Language;
}
Expand Down Expand Up @@ -625,7 +625,7 @@ export function ioPageContribution(
if (decl?.links?.length) out.links = decl.links;
if (decl?.config?.length) out.config = decl.config;
if (decl?.promptDocs?.length) out.promptDocs = decl.promptDocs;
if (decl?.storage?.length) out.storage = decl.storage;
if (decl?.storage?.length) out.storage = decl.storage.map((p) => ({ ...p, owner: `world:${worldId}` }));
if (info?.declared !== undefined) out.declared = info.declared;
if (info?.status === 'missing') out.reason = info.reason;
if (info?.status === 'active') {
Expand Down Expand Up @@ -665,7 +665,7 @@ export function personaPageContribution(
if (declaredPanels.length) out.panels = normalizePanelDecls(declaredPanels);
if (decl.config?.length) out.config = decl.config;
if (decl.promptDocs?.length) out.promptDocs = decl.promptDocs;
if (decl.storage?.length) out.storage = decl.storage;
if (decl.storage?.length) out.storage = decl.storage.map((p) => ({ ...p, owner: 'persona' }));
const invoke = decl.invoke;
if (invoke) {
out.invoke = (panel, method, args) =>
Expand Down Expand Up @@ -695,7 +695,7 @@ export function memoryPageContribution(
};
if (panels.length) out.panels = normalizePanelDecls(panels);
if (promptDocs.length) out.promptDocs = promptDocs;
if (storage.length) out.storage = storage;
if (storage.length) out.storage = storage.map((p) => ({ ...p, owner: 'memory' }));
const invoke = persona.webUi?.(language)?.invoke;
if (invoke) out.invoke = (panel, method, args) => invoke(panel, method, args);
return out;
Expand Down
1 change: 1 addition & 0 deletions src/strings.de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export const botText: Partial<typeof botEn> = {
};
export const assemblyText: Partial<typeof assemblyEn> = {
constructFailed: (detail: string) => `Erstellung fehlgeschlagen: ${detail}`,
instanceIdMismatch: (instanceId: string, definitionId: string) => `Die von create() gelieferte Instanz-ID „${instanceId}“ weicht von der Definitions-ID „${definitionId}“ ab; die ID einer World-Instanz muss der ID ihrer Definition entsprechen`,
notImplemented: 'Lokal wurde keine Implementierung dieser World gefunden.',
unknownWorld: (id: string) => `Unbekannte World: ${id}`,
alreadyRunning: (label: string) => `${label} ist bereits aktiviert`,
Expand Down
1 change: 1 addition & 0 deletions src/strings.es-419.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const botText: Partial<typeof botEn> = {

export const assemblyText: Partial<typeof assemblyEn> = {
constructFailed: (detail: string) => `Falló la construcción: ${detail}`,
instanceIdMismatch: (instanceId: string, definitionId: string) => `El id de instancia "${instanceId}" que devuelve create() no coincide con el id de definición "${definitionId}"; el id de una instancia de World debe ser el de su definición`,
notImplemented: 'No se encontró localmente una implementación de este World.',
unknownWorld: (id: string) => `World desconocido: ${id}`,
alreadyRunning: (label: string) => `${label} ya está activado`,
Expand Down
1 change: 1 addition & 0 deletions src/strings.fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export const botText: Partial<typeof botEn> = {
};
export const assemblyText: Partial<typeof assemblyEn> = {
constructFailed: (detail: string) => `Échec de la construction : ${detail}`,
instanceIdMismatch: (instanceId: string, definitionId: string) => `L'id d'instance « ${instanceId} » renvoyé par create() diffère de l'id de définition « ${definitionId} » ; l'id d'une instance de World doit être celui de sa définition`,
notImplemented: "Aucune implémentation de ce World n'a été trouvée localement.",
unknownWorld: (id: string) => `World inconnu : ${id}`,
alreadyRunning: (label: string) => `${label} est déjà activé`,
Expand Down
1 change: 1 addition & 0 deletions src/strings.it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export const botText: Partial<typeof botEn> = {
};
export const assemblyText: Partial<typeof assemblyEn> = {
constructFailed: (detail: string) => `Costruzione non riuscita: ${detail}`,
instanceIdMismatch: (instanceId: string, definitionId: string) => `L'id di istanza "${instanceId}" restituito da create() è diverso dall'id di definizione "${definitionId}"; l'id di un'istanza di World deve coincidere con quello della definizione`,
notImplemented: 'Nessuna implementazione di questo World trovata in locale.',
unknownWorld: (id: string) => `World sconosciuto: ${id}`,
alreadyRunning: (label: string) => `${label} è già attivato`,
Expand Down
1 change: 1 addition & 0 deletions src/strings.ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export const botText: Partial<typeof botEn> = {

export const assemblyText: Partial<typeof assemblyEn> = {
constructFailed: (detail: string) => `構築に失敗しました:${detail}`,
instanceIdMismatch: (instanceId: string, definitionId: string) => `create() が返したインスタンス id「${instanceId}」が定義 id「${definitionId}」と一致しません。World インスタンスの id は定義の id と同じにしてください`,
notImplemented: 'この World の実装がローカルに見つかりません。',
unknownWorld: (id: string) => `不明な World:${id}`,
alreadyRunning: (label: string) => `${label} は既に有効です`,
Expand Down
1 change: 1 addition & 0 deletions src/strings.ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export const botText: Partial<typeof botEn> = {

export const assemblyText: Partial<typeof assemblyEn> = {
constructFailed: (detail: string) => `생성 실패: ${detail}`,
instanceIdMismatch: (instanceId: string, definitionId: string) => `create()가 반환한 인스턴스 id "${instanceId}"가 정의 id "${definitionId}"와 다릅니다. World 인스턴스의 id는 정의의 id와 같아야 합니다`,
notImplemented: '이 World의 구현을 로컬에서 찾지 못했습니다.',
unknownWorld: (id: string) => `알 수 없는 World: ${id}`,
alreadyRunning: (label: string) => `${label}은(는) 이미 활성화되어 있습니다`,
Expand Down
1 change: 1 addition & 0 deletions src/strings.pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export const botText: Partial<typeof botEn> = {

export const assemblyText: Partial<typeof assemblyEn> = {
constructFailed: (detail: string) => `Falha na construção: ${detail}`,
instanceIdMismatch: (instanceId: string, definitionId: string) => `O id de instância "${instanceId}" retornado por create() difere do id de definição "${definitionId}"; o id de uma instância de World deve ser o da sua definição`,
notImplemented: 'Nenhuma implementação deste World foi encontrada localmente.',
unknownWorld: (id: string) => `World desconhecido: ${id}`,
alreadyRunning: (label: string) => `${label} já está ativado`,
Expand Down
1 change: 1 addition & 0 deletions src/strings.ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export const botText: Partial<typeof botEn> = {

export const assemblyText: Partial<typeof assemblyEn> = {
constructFailed: (detail: string) => `Ошибка создания: ${detail}`,
instanceIdMismatch: (instanceId: string, definitionId: string) => `Id экземпляра «${instanceId}», который вернул create(), не совпадает с id определения «${definitionId}»; id экземпляра World должен совпадать с id определения`,
notImplemented: 'Реализация этого World локально не найдена.',
unknownWorld: (id: string) => `Неизвестный World: ${id}`,
alreadyRunning: (label: string) => `${label}: уже включено`,
Expand Down
2 changes: 2 additions & 0 deletions src/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ export const botText = (language: Language) => pick(language, {
/** 装配层给 WebUI 的回执与拒绝理由,按发起请求的界面语言取。 */
const assemblyZh = {
constructFailed: (detail: string) => `构造失败: ${detail}`,
instanceIdMismatch: (instanceId: string, definitionId: string) => `create() 返回的实例 id「${instanceId}」与定义 id「${definitionId}」不一致,World 实例的 id 须等于定义的 id`,
notImplemented: '本地没有找到这个 World 的实现。',
unknownWorld: (id: string) => `未知 World: ${id}`,
alreadyRunning: (label: string) => `${label} 已启用`,
Expand All @@ -240,6 +241,7 @@ const assemblyZh = {
};
export const assemblyEn: typeof assemblyZh = {
constructFailed: (detail: string) => `Construction failed: ${detail}`,
instanceIdMismatch: (instanceId: string, definitionId: string) => `Instance id "${instanceId}" returned by create() differs from definition id "${definitionId}"; a World instance's id must equal its definition's id`,
notImplemented: 'No implementation of this World was found locally.',
unknownWorld: (id: string) => `Unknown World: ${id}`,
alreadyRunning: (label: string) => `${label} is already enabled`,
Expand Down
1 change: 1 addition & 0 deletions src/strings.zh-Hant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ export const botText: Partial<typeof botEn> = {

export const assemblyText: Partial<typeof assemblyEn> = {
constructFailed: (detail: string) => `建構失敗: ${detail}`,
instanceIdMismatch: (instanceId: string, definitionId: string) => `create() 回傳的實例 id「${instanceId}」與定義 id「${definitionId}」不一致,World 實例的 id 須等於定義的 id`,
notImplemented: '本機沒有找到這個 World 的實作。',
unknownWorld: (id: string) => `未知 World: ${id}`,
alreadyRunning: (label: string) => `${label} 已啟用`,
Expand Down
6 changes: 3 additions & 3 deletions src/web/client/features/storage/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export function createStorageView(deps: StorageViewDeps): StorageView {
const lock = ui.disable(btn);
try {
const out = await post<{ result?: string }>(
`/api/storage/clear?key=${encodeURIComponent(p.key)}`,
`/api/storage/clear?owner=${encodeURIComponent(p.owner)}&key=${encodeURIComponent(p.key)}`,
undefined,
{ signal },
);
Expand All @@ -112,15 +112,15 @@ export function createStorageView(deps: StorageViewDeps): StorageView {
if (!ok || signal.aborted) return;
const lock = ui.disable(nukeBtn);
try {
const out = await post<{ results?: Array<{ key: string; ok: boolean }> }>(
const out = await post<{ results?: Array<{ owner: string; key: string; ok: boolean }> }>(
'/api/storage/clear-all',
undefined,
{ signal },
);
if (signal.aborted) return;
const results = out.results || [];
const bad = results.filter((x) => !x.ok);
setMsg(bad.length ? S.partialFailed(bad.map((x) => x.key).join(',')) : S.nukedAll(results.length), bad.length > 0);
setMsg(bad.length ? S.partialFailed(bad.map((x) => `${x.owner} ${x.key}`).join(',')) : S.nukedAll(results.length), bad.length > 0);
await load();
} catch (err) {
if (isAbort(err) || signal.aborted) return;
Expand Down
36 changes: 20 additions & 16 deletions src/web/client/webui-pages/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
WEBUI_PROTOCOL_VERSION,
type WebUiManifest,
type WebUiPageManifest,
type WebUiStorageRef,
type WebUiPanelManifest,
} from '../../shared/webui-protocol.ts';
import type { WebUiMemo, WebUiPanel, WebUiPanelContext, Disposable } from '../../shared/client-panel.ts';
Expand Down Expand Up @@ -218,12 +219,12 @@ export class WebUiPageHost {
const panels = tabbed(page);
const prompts = asArray(page.prompts);
const configGroups = this.configGroupsOf(page);
const storageKeys = this.storageKeysOf(page);
const storage = this.storageOf(page);
const tools = this.toolsTabOf(page);
const wanted = panelId ?? panels[0]?.id
?? (configGroups.length ? PROVIDER_CONFIG_ROUTE : undefined)
?? (prompts.length ? PROVIDER_PROMPTS_ROUTE : undefined)
?? (storageKeys.length ? PROVIDER_STORAGE_ROUTE : undefined)
?? (storage.length ? PROVIDER_STORAGE_ROUTE : undefined)
?? (tools ? PROVIDER_TOOLS_ROUTE : undefined);
if (wanted === PROVIDER_CONFIG_ROUTE && configGroups.length) {
this.renderChrome(pageId, PROVIDER_CONFIG_ROUTE);
Expand All @@ -235,9 +236,9 @@ export class WebUiPageHost {
await this.showPrompts(pageId, prompts.map((doc) => doc.key), gen);
return;
}
if (wanted === PROVIDER_STORAGE_ROUTE && storageKeys.length) {
if (wanted === PROVIDER_STORAGE_ROUTE && storage.length) {
this.renderChrome(pageId, PROVIDER_STORAGE_ROUTE);
await this.showStorage(pageId, storageKeys, gen);
await this.showStorage(pageId, storage, gen);
return;
}
if (wanted === PROVIDER_TOOLS_ROUTE && tools) {
Expand All @@ -249,7 +250,7 @@ export class WebUiPageHost {
if (!panel) {
this.renderChrome(pageId, wanted);
if (panels.length === 0 && prompts.length === 0 && configGroups.length === 0
&& storageKeys.length === 0 && !tools) {
&& storage.length === 0 && !tools) {
this.appendNote(S.noPanels(page.label));
} else {
this.renderError(
Expand All @@ -258,7 +259,7 @@ export class WebUiPageHost {
...panels.map((p) => p.id),
...(configGroups.length ? [PROVIDER_CONFIG_ROUTE] : []),
...(prompts.length ? [PROVIDER_PROMPTS_ROUTE] : []),
...(storageKeys.length ? [PROVIDER_STORAGE_ROUTE] : []),
...(storage.length ? [PROVIDER_STORAGE_ROUTE] : []),
...(tools ? [PROVIDER_TOOLS_ROUTE] : []),
].join(' / ')),
);
Expand Down Expand Up @@ -389,10 +390,10 @@ export class WebUiPageHost {
return asArray(page.configGroups);
}

/** 仅返回当前页声明且部署支持 storage 能力的存储项 key。 */
private storageKeysOf(page: WebUiPageManifest): readonly string[] {
/** 仅返回当前页声明且部署支持 storage 能力的存储项。 */
private storageOf(page: WebUiPageManifest): readonly WebUiStorageRef[] {
if (this.snapshot?.framework?.capabilities?.storage === false) return [];
return asArray(page.storageKeys);
return asArray(page.storageParts);
}

/** Persona 页才有工具表页签,且要部署挂着 /api/tool-schemas。 */
Expand Down Expand Up @@ -427,8 +428,8 @@ export class WebUiPageHost {
if (gen !== this.generation) lifecycle.dispose();
}

/** 这一页声明的存储项走同一张 /api/storage 清单;这里只按 key 挑出自己的。 */
private async showStorage(pageId: string, keys: readonly string[], gen: number): Promise<void> {
/** 这一页声明的存储项走同一张 /api/storage 清单;这里按归属与 key 挑出自己的。 */
private async showStorage(pageId: string, refs: readonly WebUiStorageRef[], gen: number): Promise<void> {
const { slot } = this.ensurePanes();
const lifecycle = new Lifecycle(this.deps.onError);
this.mounted = { pageId, panelId: PROVIDER_STORAGE_ROUTE, lifecycle };
Expand All @@ -438,8 +439,11 @@ export class WebUiPageHost {
signal: lifecycle.signal,
doc: this.deps.doc,
});
const wanted = new Set(keys);
const view = createStorageView({ ui, signal: lifecycle.signal, filter: (part) => wanted.has(part.key) });
const view = createStorageView({
ui,
signal: lifecycle.signal,
filter: (part) => refs.some((r) => r.owner === part.owner && r.key === part.key),
});
const sheet = ui.sheet({ title: S.storageTitle, en: 'storage', desc: S.storageDesc });
sheet.body.appendChild(view.el);
slot.appendChild(sheet.el);
Expand Down Expand Up @@ -576,10 +580,10 @@ export class WebUiPageHost {
const panels = tabbed(page);
const prompts = asArray(page.prompts);
const configGroups = this.configGroupsOf(page);
const storageKeys = this.storageKeysOf(page);
const storage = this.storageOf(page);
const tools = this.toolsTabOf(page);
if (panels.length + (configGroups.length ? 1 : 0) + (prompts.length ? 1 : 0)
+ (storageKeys.length ? 1 : 0) + (tools ? 1 : 0) > 1) {
+ (storage.length ? 1 : 0) + (tools ? 1 : 0) > 1) {
const tabs = ui.rowbar();
const go = (panelId: string): void => this.deps.router.navigate(this.routeOf(page.id, panelId));
for (const p of panels) {
Expand All @@ -604,7 +608,7 @@ export class WebUiPageHost {
onClick: () => go(PROVIDER_PROMPTS_ROUTE),
}));
}
if (storageKeys.length) {
if (storage.length) {
tabs.appendChild(ui.button(S.storageTab, {
size: 'sm',
variant: activePanel === PROVIDER_STORAGE_ROUTE ? 'primary' : 'plain',
Expand Down
Loading
Loading