@apollo-music/dsh-sidecar-terms 0.1.2 → 0.1.4
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.
- package/README.md +9 -14
- package/dist/api.d.ts +1 -0
- package/dist/api.js +13 -21
- package/dist/coordinator.js +0 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -2
- package/dist/types.d.ts +0 -1
- package/package.json +16 -6
- package/dist/fallback.d.ts +0 -3
- package/dist/fallback.js +0 -169
package/README.md
CHANGED
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
5. request 尚未固定时 inject,step 已开始或已有候选回答时 steer;
|
|
10
10
|
6. steer 修订后不再审计同一 turn 的后续 assistant 输出,避免规则回执自我触发;下一 turn 自动恢复;
|
|
11
11
|
7. 最终答案前只 steer 尚未交付的 canonical term / Rule delta。
|
|
12
|
-
8. Catalog
|
|
12
|
+
8. Catalog、Resolver、鉴权或网络不可用时失败关闭,不生成离线替代规则。
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
插件固定只读 `https://apollomusic-poc1.fde0.com/kb/terminology`。`apiUrl` 不再是运行时配置项,线上服务返回 `degraded` 也会按错误处理;插件不包含本地术语表或 P0 fallback。
|
|
15
15
|
|
|
16
16
|
## 安装
|
|
17
17
|
|
|
18
18
|
在 DeepSeek Harness 项目中安装:
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npm install @apollo-music/dsh-sidecar-terms
|
|
21
|
+
npm install @apollo-music/dsh-sidecar-terms@0.1.4
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
## 配置
|
|
@@ -29,7 +29,7 @@ npm install @apollo-music/dsh-sidecar-terms
|
|
|
29
29
|
export APOLLO_TERMINOLOGY_READ_API_KEY='<READ_ONLY_API_KEY>'
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
只需把线上服务的只读凭据注入 Harness 进程。服务地址由插件版本锁定,不能通过环境变量或 Composition 改写。
|
|
33
33
|
|
|
34
34
|
在 DeepSeek Harness 的 `cordis.yml` 插件列表中加入。Cordis 的 `name` 支持 npm 包名:
|
|
35
35
|
|
|
@@ -37,7 +37,6 @@ export APOLLO_TERMINOLOGY_READ_API_KEY='<READ_ONLY_API_KEY>'
|
|
|
37
37
|
- id: apollo-terminology-rules
|
|
38
38
|
name: '@apollo-music/dsh-sidecar-terms'
|
|
39
39
|
config:
|
|
40
|
-
apiUrl: 'http://localhost:3092'
|
|
41
40
|
apiKeyEnv: 'APOLLO_TERMINOLOGY_READ_API_KEY'
|
|
42
41
|
queryIntervalMs: 0
|
|
43
42
|
catalogRefreshMs: 60000
|
|
@@ -55,17 +54,13 @@ npm ci
|
|
|
55
54
|
npm run build:harness-plugin
|
|
56
55
|
```
|
|
57
56
|
|
|
58
|
-
|
|
57
|
+
凭据示例:
|
|
59
58
|
|
|
60
59
|
```bash
|
|
61
|
-
|
|
62
|
-
export APOLLO_TERMINOLOGY_API_URL='http://localhost:3092'
|
|
63
|
-
|
|
64
|
-
# 未来公网
|
|
65
|
-
export APOLLO_TERMINOLOGY_API_URL='https://terminology.example.com'
|
|
60
|
+
export APOLLO_TERMINOLOGY_READ_API_KEY='<READ_ONLY_API_KEY>'
|
|
66
61
|
```
|
|
67
62
|
|
|
68
|
-
插件不会注册模型可见 Tool,也不拦截或改写最终答案。命中后注入的内容以 `[阿波罗术语库:原文增量]` 开头,仅包含 API 返回的新增 term/rule
|
|
63
|
+
插件不会注册模型可见 Tool,也不拦截或改写最终答案。命中后注入的内容以 `[阿波罗术语库:原文增量]` 开头,仅包含 API 返回的新增 term/rule 数据;知识库版本或 revision 没有变化时也不重复。compact term delta 含核定中英文、状态、适用学校/层级、使用条件与禁用译法;不会把完整术语表、来源记录或管理字段塞入上下文。`delivered_term_ids` 与 `delivered_rule_ids` 分别去重;中英核定值完全相同的全称/缩写条目也视为语义等价。
|
|
69
64
|
|
|
70
65
|
由于 Harness 的运行时 context 与 steer 消息使用 user role,宿主 persona 必须在 **system prompt** 中预先声明执行协议;不要把协议文字重复放进每批 Delta。使用 `complete: true` 的 persona 时,协议必须直接写入该完整 persona,例如:
|
|
71
66
|
|
|
@@ -79,9 +74,9 @@ export APOLLO_TERMINOLOGY_API_URL='https://terminology.example.com'
|
|
|
79
74
|
|
|
80
75
|
`auditReasoning: true` 只匹配每个 step 已提交的完整 reasoning block;流式 `reasoning-delta` 不参与匹配。thinking 原文只发送给 Resolver 做瞬时匹配,不会回注给模型。若部署边界不允许 thinking 离开 Harness 进程,可设为 `false`。
|
|
81
76
|
|
|
82
|
-
该插件声明依赖 `agents`、`sessions` 与 `tools`;Cordis 会等相关服务存在后再加载。官方 Harness 目前仍处于 Developer Preview,本项目锁定并测试 `0.1.
|
|
77
|
+
该插件声明依赖 `agents`、`sessions` 与 `tools`;Cordis 会等相关服务存在后再加载。官方 Harness 目前仍处于 Developer Preview,本项目锁定并测试 `0.1.1-rc.2` 的公开 `Agent`、`session/event`、`tools/post-execute`、`agent/pre-step`、`agent/turn-stopping` 接口。包清单显式声明这一组接口的完整 DSH peer 闭包,避免依赖宿主包管理器自动补齐隐式 peer。
|
|
83
78
|
|
|
84
|
-
|
|
79
|
+
配置线上只读凭据后验证:
|
|
85
80
|
|
|
86
81
|
```bash
|
|
87
82
|
npm run test:api
|
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CatalogResponse, PluginConfig, ResolveResponse } from "./types.js";
|
|
2
|
+
export declare const APOLLO_TERMINOLOGY_API_URL = "https://apollomusic-poc1.fde0.com/kb/terminology";
|
|
2
3
|
export declare class TerminologyApi {
|
|
3
4
|
private readonly config;
|
|
4
5
|
private readonly apiKey;
|
package/dist/api.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
export const APOLLO_TERMINOLOGY_API_URL = "https://apollomusic-poc1.fde0.com/kb/terminology";
|
|
2
2
|
export class TerminologyApi {
|
|
3
3
|
config;
|
|
4
4
|
apiKey;
|
|
@@ -18,35 +18,30 @@ export class TerminologyApi {
|
|
|
18
18
|
if (!Array.isArray(value.entries) || typeof value.catalog_revision !== "number") {
|
|
19
19
|
throw new Error("Terminology catalog response is malformed");
|
|
20
20
|
}
|
|
21
|
+
if (value.degraded) {
|
|
22
|
+
throw new Error(`Terminology catalog returned a degraded response: ${value.fallback_reason ?? "reason unavailable"}`);
|
|
23
|
+
}
|
|
21
24
|
this.catalog = { value, fetchedAt: Date.now() };
|
|
22
25
|
return value;
|
|
23
|
-
}).catch((error) => {
|
|
24
|
-
const fallback = this.catalog?.value ?? {
|
|
25
|
-
...FALLBACK_CATALOG,
|
|
26
|
-
fallback_reason: errorMessage(error),
|
|
27
|
-
};
|
|
28
|
-
const degraded = {
|
|
29
|
-
...fallback,
|
|
30
|
-
degraded: true,
|
|
31
|
-
fallback_reason: `live catalog unavailable: ${errorMessage(error)}`,
|
|
32
|
-
};
|
|
33
|
-
this.catalog = { value: degraded, fetchedAt: Date.now() };
|
|
34
|
-
return degraded;
|
|
35
26
|
}).finally(() => {
|
|
36
27
|
this.catalogRequest = undefined;
|
|
37
28
|
});
|
|
38
29
|
return this.catalogRequest;
|
|
39
30
|
}
|
|
40
|
-
resolve(body) {
|
|
41
|
-
|
|
31
|
+
async resolve(body) {
|
|
32
|
+
const value = await this.request("resolve", {
|
|
42
33
|
method: "POST",
|
|
43
34
|
body: JSON.stringify(body),
|
|
44
|
-
})
|
|
35
|
+
});
|
|
36
|
+
if (value.degraded) {
|
|
37
|
+
throw new Error(`Terminology resolver returned a degraded response: ${value.fallback_reason ?? "reason unavailable"}`);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
45
40
|
}
|
|
46
41
|
async request(path, init = {}) {
|
|
47
|
-
const
|
|
48
|
-
const response = await this.fetchImpl(`${baseUrl}/api/v1/${path}`, {
|
|
42
|
+
const response = await this.fetchImpl(`${APOLLO_TERMINOLOGY_API_URL}/api/v1/${path}`, {
|
|
49
43
|
...init,
|
|
44
|
+
redirect: "error",
|
|
50
45
|
headers: {
|
|
51
46
|
Accept: "application/json",
|
|
52
47
|
"X-API-Key": this.apiKey,
|
|
@@ -64,6 +59,3 @@ export class TerminologyApi {
|
|
|
64
59
|
return value;
|
|
65
60
|
}
|
|
66
61
|
}
|
|
67
|
-
function errorMessage(error) {
|
|
68
|
-
return error instanceof Error ? error.message : String(error);
|
|
69
|
-
}
|
package/dist/coordinator.js
CHANGED
|
@@ -177,9 +177,6 @@ export class TerminologyCoordinator {
|
|
|
177
177
|
delivered_rule_ids: this.knownRuleIds(state),
|
|
178
178
|
delivered_term_ids: this.knownTermIds(state),
|
|
179
179
|
});
|
|
180
|
-
if (response.degraded) {
|
|
181
|
-
this.logger.warn("terminology resolver degraded through seq %d: %s", throughSeq, response.fallback_reason ?? "bundled P0 snapshot");
|
|
182
|
-
}
|
|
183
180
|
const checkpoint = {
|
|
184
181
|
throughSeq,
|
|
185
182
|
complete: response.rule_delta.length === 0 && (response.term_delta?.length ?? 0) === 0,
|
|
@@ -295,9 +292,6 @@ export function renderDelta(response, metadataUpdate = "initial") {
|
|
|
295
292
|
else if (metadataUpdate === "changed") {
|
|
296
293
|
lines.push(`知识库状态更新:版本 ${response.kb_version},catalog revision ${response.catalog_revision}。`);
|
|
297
294
|
}
|
|
298
|
-
if (response.degraded && metadataUpdate !== "unchanged") {
|
|
299
|
-
lines.push("远程术语库暂不可用;当前使用内置 P0 保守规则。");
|
|
300
|
-
}
|
|
301
295
|
for (const term of response.term_delta ?? [])
|
|
302
296
|
lines.push(...renderTerm(term));
|
|
303
297
|
for (const rule of response.rule_delta)
|
|
@@ -308,7 +302,6 @@ function metadataSnapshot(response) {
|
|
|
308
302
|
return {
|
|
309
303
|
kbVersion: response.kb_version,
|
|
310
304
|
catalogRevision: response.catalog_revision,
|
|
311
|
-
degraded: response.degraded === true,
|
|
312
305
|
};
|
|
313
306
|
}
|
|
314
307
|
function metadataDelta(current, response) {
|
|
@@ -316,7 +309,6 @@ function metadataDelta(current, response) {
|
|
|
316
309
|
return "initial";
|
|
317
310
|
return current.kbVersion !== response.kb_version
|
|
318
311
|
|| current.catalogRevision !== response.catalog_revision
|
|
319
|
-
|| current.degraded !== (response.degraded === true)
|
|
320
312
|
? "changed"
|
|
321
313
|
: "unchanged";
|
|
322
314
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type Config = PluginConfig;
|
|
|
8
8
|
export declare const Config: Schema<Config>;
|
|
9
9
|
export declare function apply(ctx: Context, config: Config): void;
|
|
10
10
|
export declare function messageText(message: Message, auditReasoning?: boolean): string;
|
|
11
|
-
export { TerminologyApi } from "./api.js";
|
|
11
|
+
export { APOLLO_TERMINOLOGY_API_URL, TerminologyApi } from "./api.js";
|
|
12
12
|
export { TerminologyCoordinator } from "./coordinator.js";
|
|
13
13
|
export { findCatalogMatches, normalizeMatchKey } from "./matcher.js";
|
|
14
14
|
export type { AgentPort, CatalogEntry, CatalogResponse, ResolveResponse } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,6 @@ import { isOwnPluginMessage, TerminologyCoordinator } from "./coordinator.js";
|
|
|
4
4
|
export const name = "apollo-terminology-rules";
|
|
5
5
|
export const inject = ["agents", "sessions", "tools"];
|
|
6
6
|
export const Config = Schema.object({
|
|
7
|
-
apiUrl: Schema.string().required().description("阿波罗术语库 API 根地址"),
|
|
8
7
|
apiKeyEnv: Schema.string().default("APOLLO_TERMINOLOGY_READ_API_KEY").description("只读 API Key 所在环境变量名"),
|
|
9
8
|
queryIntervalMs: Schema.natural().min(0).max(60_000).default(0).description("常规增量查询的最小间隔(毫秒);默认即时查询"),
|
|
10
9
|
catalogRefreshMs: Schema.natural().min(1_000).max(3_600_000).default(60_000).description("本地 matcher catalog 缓存时间"),
|
|
@@ -165,6 +164,6 @@ function blockText(block, auditReasoning) {
|
|
|
165
164
|
return block.content.flatMap((item) => blockText(item, auditReasoning));
|
|
166
165
|
return [];
|
|
167
166
|
}
|
|
168
|
-
export { TerminologyApi } from "./api.js";
|
|
167
|
+
export { APOLLO_TERMINOLOGY_API_URL, TerminologyApi } from "./api.js";
|
|
169
168
|
export { TerminologyCoordinator } from "./coordinator.js";
|
|
170
169
|
export { findCatalogMatches, normalizeMatchKey } from "./matcher.js";
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apollo-music/dsh-sidecar-terms",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "DeepSeek Harness sidecar plugin for incremental Apollo terminology and rule injection",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -36,14 +36,24 @@
|
|
|
36
36
|
"sidecar"
|
|
37
37
|
],
|
|
38
38
|
"scripts": {
|
|
39
|
-
"build": "tsc -p tsconfig.json"
|
|
39
|
+
"build": "rm -rf dist && tsc -p tsconfig.json",
|
|
40
|
+
"prepack": "npm run build"
|
|
40
41
|
},
|
|
41
42
|
"peerDependencies": {
|
|
42
43
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
43
|
-
"@deepseek-ai/dsh-agent": "
|
|
44
|
-
"@deepseek-ai/dsh-
|
|
45
|
-
"@deepseek-ai/dsh-
|
|
46
|
-
"@deepseek-ai/dsh-
|
|
44
|
+
"@deepseek-ai/dsh-agent": "0.1.1-rc.2",
|
|
45
|
+
"@deepseek-ai/dsh-attachment": "0.1.1-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-brand": "0.1.1-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh-code-runtime": "0.1.1-rc.2",
|
|
48
|
+
"@deepseek-ai/dsh-invariants": "0.1.1-rc.2",
|
|
49
|
+
"@deepseek-ai/dsh-llm": "0.1.1-rc.2",
|
|
50
|
+
"@deepseek-ai/dsh-scope": "0.1.1-rc.2",
|
|
51
|
+
"@deepseek-ai/dsh-session": "0.1.1-rc.2",
|
|
52
|
+
"@deepseek-ai/dsh-system-prompt": "0.1.1-rc.2",
|
|
53
|
+
"@deepseek-ai/dsh-timeout": "0.1.1-rc.2",
|
|
54
|
+
"@deepseek-ai/dsh-tools": "0.1.1-rc.2",
|
|
55
|
+
"@deepseek-ai/dsh-typert-protocol": "0.1.1-rc.2",
|
|
56
|
+
"@deepseek-ai/dsh-user-approval": "0.1.1-rc.2",
|
|
47
57
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
48
58
|
}
|
|
49
59
|
}
|
package/dist/fallback.d.ts
DELETED
package/dist/fallback.js
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
import { findCatalogMatches, normalizeMatchKey } from "./matcher.js";
|
|
2
|
-
const VERSION = "0.1.0-p0-fallback";
|
|
3
|
-
const P0_RULES = [
|
|
4
|
-
{
|
|
5
|
-
rule_id: "RULE-001",
|
|
6
|
-
priority: "P0",
|
|
7
|
-
trigger: "命中已锁定术语",
|
|
8
|
-
instruction: "严格使用知识库核定译法;当前离线快照不含完整译值时保留德文,不得自行改写。",
|
|
9
|
-
bad_example: "Musiktheater→音乐剧",
|
|
10
|
-
good_example: "Musiktheater→歌剧",
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
rule_id: "RULE-002",
|
|
14
|
-
priority: "P0",
|
|
15
|
-
trigger: "Musiktheater 与 Musical",
|
|
16
|
-
instruction: "先判断原词:Musiktheater 在音乐学院专业语境译歌剧;Musical 才译音乐剧。",
|
|
17
|
-
bad_example: "Gesang/Musiktheater→声乐/音乐剧",
|
|
18
|
-
good_example: "Gesang/Musiktheater→声乐/歌剧",
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
rule_id: "RULE-003",
|
|
22
|
-
priority: "P0",
|
|
23
|
-
trigger: "缩写 CoPeCo/KMM/KPA/EUJAM/EMP",
|
|
24
|
-
instruction: "首次出现必须输出中文全称+规范缩写;缩写大小写按 canonical term。",
|
|
25
|
-
bad_example: "CopeCo;只写 KMM",
|
|
26
|
-
good_example: "当代表演与作曲联合硕士(CoPeCo);文化与媒体管理(KMM)",
|
|
27
|
-
},
|
|
28
|
-
{
|
|
29
|
-
rule_id: "RULE-004",
|
|
30
|
-
priority: "P0",
|
|
31
|
-
trigger: "语境型术语但缺少学校/专业/学位上下文",
|
|
32
|
-
instruction: "保留德文原词,并返回 needs_review=true;不得猜译。",
|
|
33
|
-
bad_example: "Diplom 一律译硕士",
|
|
34
|
-
good_example: "Diplom(层级待逐校确认)",
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
rule_id: "RULE-005",
|
|
38
|
-
priority: "P0",
|
|
39
|
-
trigger: "离线快照不能确认术语",
|
|
40
|
-
instruction: "保留官方德文全称,标记 pending;禁止只保留未知缩写或自动生成中文名。",
|
|
41
|
-
bad_example: "未知项目自动生成中文名",
|
|
42
|
-
good_example: "保留德文+中文译名待确认",
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
rule_id: "RULE-013",
|
|
46
|
-
priority: "P0",
|
|
47
|
-
trigger: "Solistenexamen/Solistenklasse/Solistenstudium 与 Konzertexamen",
|
|
48
|
-
instruction: "按官方原词分别保存,逐校核对层级;不得自动建立等价映射。",
|
|
49
|
-
bad_example: "Solistenexamen 一律译演奏家文凭",
|
|
50
|
-
good_example: "Solistenexamen(层级逐校确认)",
|
|
51
|
-
},
|
|
52
|
-
];
|
|
53
|
-
const FALLBACK_TERMS = {
|
|
54
|
-
"TERM-001": term("TERM-001", "Musiktheater", "歌剧", "Opera", "locked", "contextual", [], "仅限德国音乐学院专业语境;与 Musical 严格区分。", ["音乐剧", "Musical Theatre", "Musical Theater"]),
|
|
55
|
-
"TERM-002": term("TERM-002", "Musical", "音乐剧", "Musical Theatre", "locked", "generic", [], "与 Musiktheater 严格区分。", ["歌剧"]),
|
|
56
|
-
"TERM-016": term("TERM-016", "CoPeCo", "当代表演与作曲联合硕士(CoPeCo)", "Master’s in Contemporary Performance and Composition (CoPeCo)", "locked", "school_specific", ["hfmt-hamburg"], "首次出现必须写中文全称与规范缩写。", ["只写CoPeCo", "CopeCo", "不翻译"], "Contemporary Performance & Composition", "Master"),
|
|
57
|
-
"TERM-017": term("TERM-017", "Contemporary Performance & Composition", "当代表演与作曲联合硕士(CoPeCo)", "Master’s in Contemporary Performance and Composition (CoPeCo)", "locked", "school_specific", ["hfmt-hamburg"], "首次出现必须写中文全称与规范缩写。", ["不翻译"], "", "Master"),
|
|
58
|
-
"TERM-018": term("TERM-018", "KMM", "文化与媒体管理(KMM)", "Culture and Media Management (KMM)", "locked", "school_specific", ["hfmt-hamburg"], "首次出现必须写中文全称与规范缩写。", ["只写KMM", "艺术管理"]),
|
|
59
|
-
"TERM-019": term("TERM-019", "Kultur- und Medienmanagement", "文化与媒体管理(KMM)", "Culture and Media Management (KMM)", "locked", "school_specific", ["hfmt-hamburg"], "使用知识库核定中文名。", ["艺术管理"]),
|
|
60
|
-
"TERM-020": term("TERM-020", "EUJAM", "欧洲爵士硕士(EUJAM)", "European Jazz Master (EUJAM)", "locked", "school_specific", ["hfmt-hamburg"], "首次出现必须写中文全称与规范缩写。", ["只写EUJAM"]),
|
|
61
|
-
"TERM-021": term("TERM-021", "European Jazz Master", "欧洲爵士硕士(EUJAM)", "European Jazz Master (EUJAM)", "locked", "school_specific", ["hfmt-hamburg"], "使用知识库核定中文名。", []),
|
|
62
|
-
"TERM-026": term("TERM-026", "Konzertexamen", "演奏家文凭", "Advanced Performance Diploma (Konzertexamen)", "locked", "generic", [], "保留德文可用于院校差异追溯。", ["音乐会考试", "演奏考试"]),
|
|
63
|
-
"TERM-027": term("TERM-027", "Solistenexamen", "独奏家考试(Solistenexamen,层级逐校确认)", "Soloist Examination (level to be verified)", "contextual", "contextual", [], "与 Konzertexamen 相关但不等同,必须逐校确认。", ["一律等同Konzertexamen", "一律译演奏家文凭", "硕士", "博士"]),
|
|
64
|
-
"TERM-036": term("TERM-036", "Künstlerisch-Pädagogische Ausbildung (KPA)", "音乐教育(KPA)", "Artistic-Pedagogical Music Education (KPA)", "locked", "school_specific", ["udk-berlin"], "UdK 语境固定译音乐教育,学位层级另列。", ["只写KPA", "艺术与教育训练"]),
|
|
65
|
-
"TERM-063": term("TERM-063", "Elementare Musikpädagogik (EMP)", "基础音乐教育(EMP)", "Elementary Music Pedagogy (EMP)", "locked", "generic", [], "首次出现保留 EMP。", ["初级音乐教育"]),
|
|
66
|
-
};
|
|
67
|
-
const rawEntries = [
|
|
68
|
-
["Künstlerisch-Pädagogische Ausbildung (KPA)", ["TERM-036"], ["RULE-001", "RULE-003"]],
|
|
69
|
-
["Contemporary Performance & Composition", ["TERM-016", "TERM-017"], ["RULE-001", "RULE-003"]],
|
|
70
|
-
["Elementare Musikpädagogik (EMP)", ["TERM-063"], ["RULE-001", "RULE-003"]],
|
|
71
|
-
["Kultur- und Medienmanagement", ["TERM-018", "TERM-019"], ["RULE-001", "RULE-003"]],
|
|
72
|
-
["European Jazz Master", ["TERM-020", "TERM-021"], ["RULE-001", "RULE-003"]],
|
|
73
|
-
["Musiktheaterstudium", ["TERM-001"], ["RULE-001", "RULE-002"]],
|
|
74
|
-
["Solistenstudium", [], ["RULE-013"]],
|
|
75
|
-
["Solistenexamen", ["TERM-027"], ["RULE-004", "RULE-013"]],
|
|
76
|
-
["Solistenklasse", [], ["RULE-013"]],
|
|
77
|
-
["Konzertexamen", ["TERM-026"], ["RULE-001", "RULE-013"]],
|
|
78
|
-
["Musiktheater", ["TERM-001"], ["RULE-001", "RULE-002"]],
|
|
79
|
-
["European Jazz Master (EUJAM)", ["TERM-020"], ["RULE-001", "RULE-003"]],
|
|
80
|
-
["Kultur- und Medienmanagement (KMM)", ["TERM-018"], ["RULE-001", "RULE-003"]],
|
|
81
|
-
["Contemporary Performance & Composition (CoPeCo)", ["TERM-016"], ["RULE-001", "RULE-003"]],
|
|
82
|
-
["Musical", ["TERM-002"], ["RULE-001", "RULE-002"]],
|
|
83
|
-
["CoPeCo", ["TERM-016"], ["RULE-001", "RULE-003"]],
|
|
84
|
-
["EUJAM", ["TERM-020"], ["RULE-001", "RULE-003"]],
|
|
85
|
-
["KMM", ["TERM-018"], ["RULE-001", "RULE-003"]],
|
|
86
|
-
["KPA", ["TERM-036"], ["RULE-001", "RULE-003"]],
|
|
87
|
-
["EMP", ["TERM-063"], ["RULE-001", "RULE-003"]],
|
|
88
|
-
];
|
|
89
|
-
const entries = rawEntries
|
|
90
|
-
.map(([display, term_ids, rule_ids]) => ({ match_key: normalizeMatchKey(display), display, term_ids, rule_ids }))
|
|
91
|
-
.sort((a, b) => b.match_key.length - a.match_key.length || a.match_key.localeCompare(b.match_key));
|
|
92
|
-
export const FALLBACK_CATALOG = {
|
|
93
|
-
kb_version: VERSION,
|
|
94
|
-
catalog_revision: 0,
|
|
95
|
-
generated_at: "2026-08-14T00:00:00.000Z",
|
|
96
|
-
entries,
|
|
97
|
-
degraded: true,
|
|
98
|
-
fallback_reason: "bundled P0 snapshot",
|
|
99
|
-
};
|
|
100
|
-
export function resolveWithP0Fallback(body, reason) {
|
|
101
|
-
const segments = Array.isArray(body.segments)
|
|
102
|
-
? body.segments.filter(isSegment)
|
|
103
|
-
: [];
|
|
104
|
-
const delivered = new Set(Array.isArray(body.delivered_rule_ids)
|
|
105
|
-
? body.delivered_rule_ids.filter((value) => typeof value === "string")
|
|
106
|
-
: []);
|
|
107
|
-
const deliveredTerms = new Set(Array.isArray(body.delivered_term_ids)
|
|
108
|
-
? body.delivered_term_ids.filter((value) => typeof value === "string")
|
|
109
|
-
: []);
|
|
110
|
-
const ruleIds = new Set();
|
|
111
|
-
const hits = [];
|
|
112
|
-
for (const segment of segments) {
|
|
113
|
-
for (const entry of findCatalogMatches(segment.text, entries)) {
|
|
114
|
-
entry.rule_ids.forEach((ruleId) => ruleIds.add(ruleId));
|
|
115
|
-
entry.term_ids.forEach((termId) => hits.push({ term_id: termId, matched_text: entry.display }));
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
const candidates = Array.isArray(body.candidate_terms)
|
|
119
|
-
? body.candidate_terms.filter((value) => typeof value === "string")
|
|
120
|
-
: [];
|
|
121
|
-
const unresolved = candidates
|
|
122
|
-
.filter((candidate) => !entries.some((entry) => entry.match_key === normalizeMatchKey(candidate)))
|
|
123
|
-
.map((matched_text) => ({ matched_text, reason: "unknown", candidate_term_ids: [] }));
|
|
124
|
-
if (unresolved.length)
|
|
125
|
-
ruleIds.add("RULE-005");
|
|
126
|
-
const dedupedHits = dedupeHits(hits);
|
|
127
|
-
return {
|
|
128
|
-
kb_version: VERSION,
|
|
129
|
-
catalog_revision: 0,
|
|
130
|
-
...(typeof body.audit_through_seq === "number" ? { audit_through_seq: body.audit_through_seq } : {}),
|
|
131
|
-
hits: dedupedHits,
|
|
132
|
-
term_delta: [...new Set(dedupedHits.map((hit) => hit.term_id))]
|
|
133
|
-
.filter((termId) => !deliveredTerms.has(termId))
|
|
134
|
-
.flatMap((termId) => FALLBACK_TERMS[termId] ? [FALLBACK_TERMS[termId]] : []),
|
|
135
|
-
rule_delta: P0_RULES.filter((rule) => ruleIds.has(rule.rule_id) && !delivered.has(rule.rule_id)),
|
|
136
|
-
unresolved,
|
|
137
|
-
degraded: true,
|
|
138
|
-
fallback_reason: reason,
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
function term(term_id, source_term, preferred_zh, preferred_en, status, scope_type, school_ids, context_rule, forbidden_translations, full_form = "", degree_level = "") {
|
|
142
|
-
return {
|
|
143
|
-
term_id,
|
|
144
|
-
source_term,
|
|
145
|
-
full_form,
|
|
146
|
-
preferred_zh,
|
|
147
|
-
preferred_en,
|
|
148
|
-
official_en: "",
|
|
149
|
-
status,
|
|
150
|
-
scope_type,
|
|
151
|
-
school_ids,
|
|
152
|
-
degree_level,
|
|
153
|
-
context_rule,
|
|
154
|
-
forbidden_translations,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
function isSegment(value) {
|
|
158
|
-
return Boolean(value && typeof value === "object" && "text" in value && typeof value.text === "string");
|
|
159
|
-
}
|
|
160
|
-
function dedupeHits(hits) {
|
|
161
|
-
const seen = new Set();
|
|
162
|
-
return hits.filter((hit) => {
|
|
163
|
-
const key = `${hit.term_id}:${hit.matched_text}`;
|
|
164
|
-
if (seen.has(key))
|
|
165
|
-
return false;
|
|
166
|
-
seen.add(key);
|
|
167
|
-
return true;
|
|
168
|
-
});
|
|
169
|
-
}
|