feat: 功能清单进智能体提示,补字幕按功能模块约定改为样板

- 外围功能模块(补字幕、原站视图、桌面模式、网页搜索、MCP、定时任务)声明 manifest,与 FEATURES 一一对应;
  系统提示「能力地图」末尾列出外围功能(没有开关,能力只列本次可用的)
- 补字幕:订阅 organization.finished(认出值得补字幕的整理,自动补字幕还没做,不建任务),
  写入字幕后在同一事务发 subtitle.written;原有行为不变

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-06 19:27:14 +08:00
1 parent 1db689d9a7
commit 7aa5182f3d
14 files changed
+237 -17

No files matched your search

+2
View File
@@ -1,3 +1,4 @@
import { featureManifest } from "@umt/contracts";
import { sourcesIdentityModule } from "../sources-identity/index.ts";
import type { ServerContext, ServerModule } from "../types.ts";
import { registerBrowserCapabilities } from "./capabilities.ts";
@@ -36,6 +37,7 @@ export function createBrowserModule(opts: BrowserModuleOptions = {}): ServerModu
};
return {
name: "browser",
manifest: featureManifest("browser"),
service,
registerRoutes(app, ctx) {
registerBrowserRoutes(app, () => service(ctx));
+2
View File
@@ -1,3 +1,4 @@
import { featureManifest } from "@umt/contracts";
import { browserServiceFor } from "../browser/index.ts";
import type { ServerContext, ServerModule } from "../types.ts";
import { dbSessionChecks, desktopUpstream, registerDesktopRoutes } from "./desktop.ts";
@@ -11,6 +12,7 @@ export function createDesktopModule(): ServerModule {
let closeAll: (() => void) | null = null;
return {
name: "desktop",
manifest: featureManifest("desktop"),
registerRoutes(app, ctx: ServerContext) {
const r = registerDesktopRoutes(app, {
upstream: desktopUpstream({
+2
View File
@@ -1,3 +1,4 @@
import { featureManifest } from "@umt/contracts";
import denoJson from "../../../deno.json" with { type: "json" };
import type { ServerModule } from "../types.ts";
import { createMcpConfigStore } from "./config-store.ts";
@@ -13,6 +14,7 @@ export function createMcpModule(opts: { version?: string } = {}): ServerModule &
const table = new McpSessionTable();
return {
name: "mcp",
manifest: featureManifest("mcp"),
sessions: table,
registerRoutes(app, ctx) {
registerMcpRoutes(app, ctx, {
@@ -1,3 +1,4 @@
import { featureManifest } from "@umt/contracts";
import type { ServerContext, ServerModule } from "../types.ts";
import { builtinAgentCheckDefs } from "./agent-checks.ts";
import { registerScheduleCapabilities } from "./capabilities.ts";
@@ -26,6 +27,7 @@ export function createSchedulesModule(opts: SchedulesModuleOptions = {}): Server
let scheduler: Scheduler | null = null;
return {
name: "schedules",
manifest: featureManifest("schedules"),
scheduler: () => scheduler,
registerRoutes(app, ctx) {
registerScheduleRoutes(app, ctx, () => scheduleRegistry(ctx));
@@ -0,0 +1,28 @@
import { type EventPayload, parseEventPayload } from "@umt/contracts";
import type { OutboxRouteFn } from "@umt/tasks";
import type { ServerContext } from "../types.ts";
// 补字幕订阅的生命周期事件(ADR-006 功能模块样板)。
// 「入库后自动补字幕」还没做(docs/plan/subtitles.md):这里只认出值得补字幕的整理,记一条调试日志,不建任务。
// 以后做自动补字幕时把 null 换成 { task: "...", input } 即可,订阅关系不用再改。
const IMPORT_ACTIONS = new Set(["copy", "move", "in_place"]);
/** 整理结束事件里值得补字幕的:成功或部分成功、入库类动作(复制 / 移动 / 原地入库)、认到了作品 */
export function subtitleCandidate(
p: EventPayload<"organization.finished">,
): { recordId: string; mediaEntityId: string } | null {
if (p.outcome === "failed" || !IMPORT_ACTIONS.has(p.action) || !p.mediaEntityId) return null;
return { recordId: p.recordId, mediaEntityId: p.mediaEntityId };
}
export function subtitlesOutboxRoutes(ctx: Pick<ServerContext, "logger">): Record<string, OutboxRouteFn> {
return {
"organization.finished": (ev) => {
const p = parseEventPayload("organization.finished", ev.payload);
const c = p ? subtitleCandidate(p) : null;
if (c) ctx.logger.debug(c, "补字幕:整理已完成,自动补字幕还没做,跳过");
return null;
},
};
}
+15 -2
View File
@@ -1,9 +1,16 @@
import { Api, type SubtitleApplyRequest, type SubtitleFetchRequest, type SubtitleSource } from "@umt/contracts";
import {
Api,
featureManifest,
type SubtitleApplyRequest,
type SubtitleFetchRequest,
type SubtitleSource,
} from "@umt/contracts";
import { ApiException } from "../../http/errors.ts";
import { route } from "../agent/http.ts";
import { human } from "../browser/routes.ts";
import { sourcesIdentityModule } from "../sources-identity/index.ts";
import type { ServerModule } from "../types.ts";
import { subtitlesOutboxRoutes } from "./events.ts";
import {
downloadFromSession,
downloadOnline,
@@ -13,10 +20,15 @@ import {
} from "./online/index.ts";
import { applySubtitles, parseSubtitles, subtitleTargets, subtitleWrites, undoSubtitleWrite } from "./service.ts";
/** 补字幕(docs/plan/subtitles.md):上传 / 在线下载的字幕解析、配对、写入、历史与撤销 */
/**
* 补字幕(docs/plan/subtitles.md):上传 / 在线下载的字幕解析、配对、写入、历史与撤销。
* 功能模块样板(ADR-006):manifest 进功能清单(给智能体),订阅 organization.finished(events.ts),
* 写入后发 subtitle.written;前端经插槽挂在作品页操作区与整理记录行菜单(apps/web/app/features/subtitles.tsx)。
*/
export function createSubtitlesModule(): ServerModule {
return {
name: "subtitles",
manifest: featureManifest("subtitles"),
registerRoutes(app, ctx) {
const tag = { tag: "subtitles" };
const rt = () => sourcesIdentityModule.runtime(ctx);
@@ -69,6 +81,7 @@ export function createSubtitlesModule(): ServerModule {
route(app, Api.subtitles.undo, ({ params }) => undoSubtitleWrite(ctx, params.id), tag);
},
registerTasks() {},
outboxRoutes: (ctx) => subtitlesOutboxRoutes(ctx),
};
}
+26 -11
View File
@@ -1,15 +1,16 @@
import { copyFile, mkdir, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import type {
SubtitleApplyRequest,
SubtitleApplyResult,
SubtitleLanguage,
SubtitleParsedItem,
SubtitleParseResponse,
SubtitleSource,
SubtitleTarget,
SubtitleTargetsResponse,
SubtitleWriteView,
import {
lifecycleKey,
type SubtitleApplyRequest,
type SubtitleApplyResult,
type SubtitleLanguage,
type SubtitleParsedItem,
type SubtitleParseResponse,
type SubtitleSource,
type SubtitleTarget,
type SubtitleTargetsResponse,
type SubtitleWriteView,
} from "@umt/contracts";
import type { SubtitleWriteTable } from "@umt/db";
import {
@@ -259,7 +260,7 @@ export async function applySubtitles(
})
.execute();
}
return await trx
const w = await trx
.insertInto("subtitle_write")
.values({
id,
@@ -277,6 +278,20 @@ export async function applySubtitles(
})
.returningAll()
.executeTakeFirstOrThrow();
// 生命周期广播(ADR-006):字幕已写入
await ctx.outbox.emit(
trx,
"subtitle.written",
{
writeId: w.id,
mediaEntityId: w.media_entity_id,
videoLocationId: t.locationId,
language: w.language as SubtitleLanguage,
source: w.source as SubtitleSource,
},
{ idempotencyKey: lifecycleKey("subtitle.written", w.id) },
);
return w;
});
written.push(writeView(row));
} catch (e) {
@@ -1,3 +1,4 @@
import { featureManifest } from "@umt/contracts";
import type { ServerContext, ServerModule } from "../types.ts";
import { registerWebSearchCapabilities } from "./capabilities.ts";
import { registerWebSearchRoutes } from "./routes.ts";
@@ -24,6 +25,7 @@ export function createWebSearchModule(opts: WebSearchModuleOptions = {}): Server
};
return {
name: "websearch",
manifest: featureManifest("websearch"),
service,
registerRoutes(app, ctx) {
registerWebSearchRoutes(app, () => service(ctx));