tt-help-cli-ycl 1.4.21 → 1.4.23
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/package.json +1 -1
- package/src/cli/comments.js +63 -1
- package/src/cli/refresh.js +1 -1
- package/src/lib/parse-ssr.mjs +6 -0
- package/src/lib/tag-discover.js +8 -4
- package/src/lib/tag-fetcher.js +5 -0
- package/src/scraper/explore-core.js +14 -4
- package/src/watch/data-store.js +80 -1
- package/src/watch/db-columns.js +2 -0
- package/src/watch/db-schema.js +1 -0
- package/src/watch/db-tags.js +237 -17
- package/src/watch/llm-config.js +319 -0
- package/src/watch/llm-scoring.js +103 -63
- package/src/watch/public/app.js +248 -23
- package/src/watch/public/index.html +39 -0
- package/src/watch/public/style.css +154 -0
- package/src/watch/server.js +109 -6
- package/src/watch/tag-service.js +119 -41
|
@@ -1623,3 +1623,157 @@ td.user-id:hover {
|
|
|
1623
1623
|
pointer-events: none;
|
|
1624
1624
|
z-index: 9999;
|
|
1625
1625
|
}
|
|
1626
|
+
|
|
1627
|
+
/* ========== LLM 设置页 ========== */
|
|
1628
|
+
#settingsPage {
|
|
1629
|
+
display: none;
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
#settingsPage.active {
|
|
1633
|
+
display: block;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
.settings-card {
|
|
1637
|
+
background: #1a1a24;
|
|
1638
|
+
border-radius: 12px;
|
|
1639
|
+
padding: 28px;
|
|
1640
|
+
max-width: 560px;
|
|
1641
|
+
margin: 0 auto;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
.settings-card h3 {
|
|
1645
|
+
font-size: 17px;
|
|
1646
|
+
color: #e0e0e0;
|
|
1647
|
+
margin-bottom: 6px;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
.settings-hint {
|
|
1651
|
+
font-size: 12px;
|
|
1652
|
+
color: #888;
|
|
1653
|
+
margin-bottom: 24px;
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
.settings-form {
|
|
1657
|
+
display: flex;
|
|
1658
|
+
flex-direction: column;
|
|
1659
|
+
gap: 18px;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
.settings-field label {
|
|
1663
|
+
display: block;
|
|
1664
|
+
font-size: 13px;
|
|
1665
|
+
color: #aaa;
|
|
1666
|
+
margin-bottom: 6px;
|
|
1667
|
+
font-weight: 600;
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
.settings-field input,
|
|
1671
|
+
.settings-field select {
|
|
1672
|
+
width: 100%;
|
|
1673
|
+
padding: 10px 14px;
|
|
1674
|
+
border: 1px solid #333;
|
|
1675
|
+
border-radius: 8px;
|
|
1676
|
+
background: #0f0f13;
|
|
1677
|
+
color: #e0e0e0;
|
|
1678
|
+
font-size: 14px;
|
|
1679
|
+
font-family: inherit;
|
|
1680
|
+
outline: none;
|
|
1681
|
+
transition: border-color 0.2s;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
.settings-field input:focus,
|
|
1685
|
+
.settings-field select:focus {
|
|
1686
|
+
border-color: #fe2c55;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
.settings-field input[readonly] {
|
|
1690
|
+
color: #888;
|
|
1691
|
+
cursor: default;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
.settings-field select {
|
|
1695
|
+
cursor: pointer;
|
|
1696
|
+
appearance: none;
|
|
1697
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
|
|
1698
|
+
background-repeat: no-repeat;
|
|
1699
|
+
background-position: right 12px center;
|
|
1700
|
+
padding-right: 36px;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
.settings-key-row {
|
|
1704
|
+
display: flex;
|
|
1705
|
+
gap: 6px;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
.settings-key-row input {
|
|
1709
|
+
flex: 1;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
.settings-key-row button {
|
|
1713
|
+
padding: 8px 12px;
|
|
1714
|
+
border: 1px solid #333;
|
|
1715
|
+
border-radius: 8px;
|
|
1716
|
+
background: #2a2a3a;
|
|
1717
|
+
color: #ccc;
|
|
1718
|
+
font-size: 16px;
|
|
1719
|
+
cursor: pointer;
|
|
1720
|
+
transition: background 0.2s;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
.settings-key-row button:hover {
|
|
1724
|
+
background: #333;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
.settings-note {
|
|
1728
|
+
display: block;
|
|
1729
|
+
font-size: 11px;
|
|
1730
|
+
color: #666;
|
|
1731
|
+
margin-top: 4px;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
.settings-status {
|
|
1735
|
+
padding: 10px 14px;
|
|
1736
|
+
border-radius: 8px;
|
|
1737
|
+
font-size: 13px;
|
|
1738
|
+
text-align: center;
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
.settings-status.ok {
|
|
1742
|
+
background: rgba(74, 222, 128, 0.1);
|
|
1743
|
+
color: #4ade80;
|
|
1744
|
+
border: 1px solid rgba(74, 222, 128, 0.2);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
.settings-status.error {
|
|
1748
|
+
background: rgba(248, 113, 113, 0.1);
|
|
1749
|
+
color: #f87171;
|
|
1750
|
+
border: 1px solid rgba(248, 113, 113, 0.2);
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
.settings-status.loading {
|
|
1754
|
+
background: rgba(250, 204, 21, 0.1);
|
|
1755
|
+
color: #facc15;
|
|
1756
|
+
border: 1px solid rgba(250, 204, 21, 0.2);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
.settings-form .btn-row {
|
|
1760
|
+
display: flex;
|
|
1761
|
+
justify-content: flex-end;
|
|
1762
|
+
margin-top: 4px;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
.settings-form .btn-submit {
|
|
1766
|
+
padding: 10px 28px;
|
|
1767
|
+
border: none;
|
|
1768
|
+
border-radius: 8px;
|
|
1769
|
+
background: #fe2c55;
|
|
1770
|
+
color: #fff;
|
|
1771
|
+
font-size: 14px;
|
|
1772
|
+
font-weight: 600;
|
|
1773
|
+
cursor: pointer;
|
|
1774
|
+
transition: background 0.2s;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
.settings-form .btn-submit:hover {
|
|
1778
|
+
background: #e61944;
|
|
1779
|
+
}
|
package/src/watch/server.js
CHANGED
|
@@ -216,6 +216,16 @@ export function startWatchServer(
|
|
|
216
216
|
return new Promise((_resolve, reject) => {
|
|
217
217
|
const store = existingStore || createStore(dataAnchor, options);
|
|
218
218
|
|
|
219
|
+
// 初始化 LLM 动态配置(SQLite > 环境变量 > 默认值)
|
|
220
|
+
import("./llm-config.js").then((m) => m.loadLlmConfig());
|
|
221
|
+
|
|
222
|
+
// 启动时清理废弃的反思规则(避免 DB 膨胀)
|
|
223
|
+
import("./db-tags.js").then((m) => {
|
|
224
|
+
if (m.cleanupInactiveReflectionRules) {
|
|
225
|
+
m.cleanupInactiveReflectionRules();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
219
229
|
function logJob(action, detail) {
|
|
220
230
|
const ts = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
|
221
231
|
const d = detail
|
|
@@ -983,6 +993,9 @@ export function startWatchServer(
|
|
|
983
993
|
followerCount: u.followerCount || 0,
|
|
984
994
|
topVideoPlayCount: u.topVideoPlayCount || null,
|
|
985
995
|
topVideoHref: u.topVideoHref || null,
|
|
996
|
+
topVideoIsAd: u.topVideoIsAd ?? null,
|
|
997
|
+
recentEcVideos: u.recentEcVideos || null,
|
|
998
|
+
latestVideoTime: u.latestVideoTime || null,
|
|
986
999
|
}));
|
|
987
1000
|
sendJSON(res, 200, { total: targets.length, users });
|
|
988
1001
|
}
|
|
@@ -1068,6 +1081,8 @@ export function startWatchServer(
|
|
|
1068
1081
|
search: params.search || undefined,
|
|
1069
1082
|
limit: parseInt(params.limit, 10),
|
|
1070
1083
|
offset: parseInt(params.offset || "0", 10),
|
|
1084
|
+
sort: params.sort || undefined,
|
|
1085
|
+
sortOrder: params.sortOrder || "desc",
|
|
1071
1086
|
},
|
|
1072
1087
|
);
|
|
1073
1088
|
// 确保每个用户对象包含 topVideoPlayCount、topVideoHref 和 topVideoIsAd
|
|
@@ -1253,6 +1268,55 @@ export function startWatchServer(
|
|
|
1253
1268
|
return;
|
|
1254
1269
|
}
|
|
1255
1270
|
|
|
1271
|
+
// ====== LLM 配置 API ======
|
|
1272
|
+
if (req.method === "GET" && routePath === "/api/llm-config") {
|
|
1273
|
+
try {
|
|
1274
|
+
const { getLlmConfig, LLM_PROVIDERS } =
|
|
1275
|
+
await import("./llm-config.js");
|
|
1276
|
+
sendJSON(res, 200, {
|
|
1277
|
+
...getLlmConfig(),
|
|
1278
|
+
providers: LLM_PROVIDERS,
|
|
1279
|
+
});
|
|
1280
|
+
} catch (e) {
|
|
1281
|
+
sendJSON(res, 500, { error: e.message });
|
|
1282
|
+
}
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
if (req.method === "POST" && routePath === "/api/llm-config") {
|
|
1287
|
+
try {
|
|
1288
|
+
const body = await readBody(req);
|
|
1289
|
+
const { updateLlmConfig, getLlmConfig, LLM_PROVIDERS } =
|
|
1290
|
+
await import("./llm-config.js");
|
|
1291
|
+
const result = updateLlmConfig({
|
|
1292
|
+
provider: body.provider,
|
|
1293
|
+
model: body.model,
|
|
1294
|
+
url: body.url,
|
|
1295
|
+
apiKey: body.apiKey,
|
|
1296
|
+
});
|
|
1297
|
+
sendJSON(res, 200, { ...result, providers: LLM_PROVIDERS });
|
|
1298
|
+
} catch (e) {
|
|
1299
|
+
sendJSON(res, 400, { error: e.message });
|
|
1300
|
+
}
|
|
1301
|
+
return;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// GET /api/llm-test — 直接测试 LLM API 连通性
|
|
1305
|
+
if (req.method === "GET" && routePath === "/api/llm-test") {
|
|
1306
|
+
try {
|
|
1307
|
+
const { callLLM } = await import("./tag-service.js");
|
|
1308
|
+
const result = await callLLM("Say hello in exactly one word.", 0.1);
|
|
1309
|
+
sendJSON(res, 200, {
|
|
1310
|
+
ok: result.length > 0,
|
|
1311
|
+
reply: result || "(空)",
|
|
1312
|
+
replyLen: result.length,
|
|
1313
|
+
});
|
|
1314
|
+
} catch (e) {
|
|
1315
|
+
sendJSON(res, 500, { ok: false, error: e.message });
|
|
1316
|
+
}
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1256
1320
|
if (req.method === "POST" && routePath === "/api/attach-stuck/restore") {
|
|
1257
1321
|
try {
|
|
1258
1322
|
const body = await readBody(req);
|
|
@@ -1502,23 +1566,31 @@ export function startWatchServer(
|
|
|
1502
1566
|
return;
|
|
1503
1567
|
}
|
|
1504
1568
|
|
|
1505
|
-
// GET /api/tags?status=&country=&limit=&offset=
|
|
1569
|
+
// GET /api/tags?status=&country=&search=&limit=&offset=
|
|
1506
1570
|
if (req.method === "GET" && routePath === "/api/tags") {
|
|
1507
1571
|
const status = params.status || null;
|
|
1508
1572
|
const country = params.country || null;
|
|
1573
|
+
const search = params.search || null;
|
|
1509
1574
|
const limit = Math.min(parseInt(params.limit) || 200, 500);
|
|
1510
1575
|
const offset = parseInt(params.offset) || 0;
|
|
1511
1576
|
const upperCountry = country ? country.toUpperCase() : null;
|
|
1577
|
+
const searchTerm = search ? search.trim().toLowerCase() : null;
|
|
1512
1578
|
|
|
1513
1579
|
let tags;
|
|
1514
1580
|
if (status) {
|
|
1515
|
-
tags = store.getTagsByStatus(
|
|
1581
|
+
tags = store.getTagsByStatus(
|
|
1582
|
+
status,
|
|
1583
|
+
limit,
|
|
1584
|
+
offset,
|
|
1585
|
+
upperCountry,
|
|
1586
|
+
searchTerm,
|
|
1587
|
+
);
|
|
1516
1588
|
} else {
|
|
1517
|
-
tags = store.getAllTags(limit, offset, upperCountry);
|
|
1589
|
+
tags = store.getAllTags(limit, offset, upperCountry, searchTerm);
|
|
1518
1590
|
}
|
|
1519
1591
|
|
|
1520
|
-
//
|
|
1521
|
-
const stats = store.getTagStats(upperCountry);
|
|
1592
|
+
// 获取总数(带搜索过滤,按 status 返回对应计数)
|
|
1593
|
+
const stats = store.getTagStats(upperCountry, searchTerm);
|
|
1522
1594
|
let total;
|
|
1523
1595
|
if (stats) {
|
|
1524
1596
|
if (status === "new") total = stats.new;
|
|
@@ -1676,6 +1748,35 @@ export function startWatchServer(
|
|
|
1676
1748
|
return;
|
|
1677
1749
|
}
|
|
1678
1750
|
|
|
1751
|
+
// POST /api/tags/batch-add-if-new — 批量添加 tag(只插入不存在的,不覆盖已有)
|
|
1752
|
+
if (req.method === "POST" && routePath === "/api/tags/batch-add-if-new") {
|
|
1753
|
+
try {
|
|
1754
|
+
const body = await readBody(req);
|
|
1755
|
+
const { tags, countries, source } = body || {};
|
|
1756
|
+
if (!Array.isArray(tags) || tags.length === 0) {
|
|
1757
|
+
sendJSON(res, 400, { error: "tags 不能为空" });
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
if (!Array.isArray(countries) || countries.length === 0) {
|
|
1761
|
+
sendJSON(res, 400, { error: "countries 不能为空" });
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
const ret = store.insertTagsIfNew(tags, countries, source || "video");
|
|
1765
|
+
if (ret.error) {
|
|
1766
|
+
sendJSON(res, 500, { error: ret.error });
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
sendJSON(res, 200, {
|
|
1770
|
+
ok: true,
|
|
1771
|
+
inserted: ret.inserted,
|
|
1772
|
+
tags: ret.tags,
|
|
1773
|
+
});
|
|
1774
|
+
} catch (e) {
|
|
1775
|
+
sendJSON(res, 500, { error: e.message });
|
|
1776
|
+
}
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1679
1780
|
// GET /api/tags/health?country=ES — 自闭环:获取国家 tag 健康指标
|
|
1680
1781
|
if (req.method === "GET" && routePath === "/api/tags/health") {
|
|
1681
1782
|
try {
|
|
@@ -2179,7 +2280,9 @@ export function startWatchServer(
|
|
|
2179
2280
|
|
|
2180
2281
|
if (
|
|
2181
2282
|
req.method === "GET" &&
|
|
2182
|
-
(routePath === "/" ||
|
|
2283
|
+
(routePath === "/" ||
|
|
2284
|
+
routePath === "/index.html" ||
|
|
2285
|
+
routePath === "/settings")
|
|
2183
2286
|
) {
|
|
2184
2287
|
// 设有密码时,检查登录态
|
|
2185
2288
|
if (PASSWORD_ENABLED) {
|
package/src/watch/tag-service.js
CHANGED
|
@@ -23,8 +23,10 @@ export const COUNTRY_LANG = {
|
|
|
23
23
|
AT: "de",
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
export const
|
|
26
|
+
// 旧常量保留兼容(外部可能引用),实际调用走 llm-config 动态配置
|
|
27
|
+
export const LLM_URL =
|
|
28
|
+
process.env.LLM_URL || "http://82.156.52.214:18000/v1/chat/completions";
|
|
29
|
+
export const LLM_MODEL = process.env.LLM_MODEL || "zc-fast";
|
|
28
30
|
|
|
29
31
|
export function getLang(country) {
|
|
30
32
|
return COUNTRY_LANG[country] || "en";
|
|
@@ -33,25 +35,74 @@ export function getLang(country) {
|
|
|
33
35
|
// ====== LLM 调用 ======
|
|
34
36
|
|
|
35
37
|
export async function callLLM(prompt, temperature = 0.7) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
})
|
|
51
|
-
|
|
38
|
+
const { getLlmUrl, getLlmApiKey, isAutoMode, getAutoPool } =
|
|
39
|
+
await import("./llm-config.js");
|
|
40
|
+
const url = getLlmUrl();
|
|
41
|
+
const apiKey = getLlmApiKey();
|
|
42
|
+
|
|
43
|
+
// auto 模式:依次尝试候选池中的模型
|
|
44
|
+
const models = isAutoMode() ? getAutoPool() : [null];
|
|
45
|
+
|
|
46
|
+
let lastError = "";
|
|
47
|
+
for (let i = 0; i < models.length; i++) {
|
|
48
|
+
const model =
|
|
49
|
+
models[i] !== null
|
|
50
|
+
? models[i]
|
|
51
|
+
: (await import("./llm-config.js")).getLlmModel();
|
|
52
|
+
const { fetch } = await import("undici");
|
|
53
|
+
|
|
54
|
+
console.error(
|
|
55
|
+
`[callLLM] → ${url} model=${model} (${i + 1}/${models.length})`,
|
|
56
|
+
);
|
|
52
57
|
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
let response;
|
|
59
|
+
try {
|
|
60
|
+
response = await fetch(url, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: {
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
Authorization: `Bearer ${apiKey}`,
|
|
65
|
+
},
|
|
66
|
+
body: JSON.stringify({
|
|
67
|
+
model,
|
|
68
|
+
messages: [{ role: "user", content: prompt }],
|
|
69
|
+
max_tokens: 1024,
|
|
70
|
+
temperature,
|
|
71
|
+
enable_thinking: false,
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
} catch (e) {
|
|
75
|
+
lastError = e.message;
|
|
76
|
+
console.error(`[callLLM] ${model} 网络失败: ${e.message},尝试下一个`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const status = response.status;
|
|
81
|
+
const result = await response.json().catch(() => null);
|
|
82
|
+
|
|
83
|
+
if (status !== 200 || !result || !result.choices?.[0]?.message?.content) {
|
|
84
|
+
const errMsg = result?.error?.message || `HTTP ${status}`;
|
|
85
|
+
lastError = errMsg;
|
|
86
|
+
console.error(`[callLLM] ${model} 失败: ${errMsg},尝试下一个`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const content = result.choices[0].message.content;
|
|
91
|
+
if (i > 0) {
|
|
92
|
+
console.error(`[callLLM] ✅ auto 切换成功: ${model}`);
|
|
93
|
+
}
|
|
94
|
+
// 记录成功的模型,下次优先用它
|
|
95
|
+
if (isAutoMode()) {
|
|
96
|
+
const { recordAutoSuccess } = await import("./llm-config.js");
|
|
97
|
+
recordAutoSuccess(model);
|
|
98
|
+
}
|
|
99
|
+
return content;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.error(
|
|
103
|
+
`[callLLM] ❌ 全部 ${models.length} 个模型均失败,最后错误: ${lastError}`,
|
|
104
|
+
);
|
|
105
|
+
return "";
|
|
55
106
|
}
|
|
56
107
|
|
|
57
108
|
export function normalizeTag(t) {
|
|
@@ -750,21 +801,31 @@ export async function discoverTagsForCountry(
|
|
|
750
801
|
};
|
|
751
802
|
|
|
752
803
|
let forceNicheSwitch = null;
|
|
753
|
-
if (health && health.consecutiveLowProductiveRounds >=
|
|
754
|
-
// 连续
|
|
755
|
-
//
|
|
804
|
+
if (health && health.consecutiveLowProductiveRounds >= 3) {
|
|
805
|
+
// 连续 >=3 轮 productive=0,强制切换方向
|
|
806
|
+
// from = LLM 当前正在攻击的 niche(从 discover_log 追踪)
|
|
756
807
|
const currentNiche =
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
808
|
+
health.currentNiche ||
|
|
809
|
+
nicheRecommendation?.saturated?.[0]?.niche ||
|
|
810
|
+
"marketplace_local";
|
|
811
|
+
|
|
812
|
+
// to = uncovered 中排除 currentNiche 后的第一个
|
|
813
|
+
const uncoveredList = nicheRecommendation?.uncovered || [];
|
|
814
|
+
const nextUncovered = uncoveredList.find((n) => n.niche !== currentNiche);
|
|
815
|
+
// 如果所有 uncovered 都被排除(极端情况),回退到 uncovered 最后一个
|
|
816
|
+
const targetNiche =
|
|
817
|
+
nextUncovered?.niche ||
|
|
818
|
+
uncoveredList[uncoveredList.length - 1]?.niche ||
|
|
819
|
+
"food";
|
|
820
|
+
|
|
760
821
|
forceNicheSwitch = {
|
|
761
822
|
from: currentNiche,
|
|
762
823
|
to: targetNiche,
|
|
763
|
-
reason: `连续 ${health.consecutiveLowProductiveRounds}
|
|
824
|
+
reason: `连续 ${health.consecutiveLowProductiveRounds} 轮无有效产出,当前位置: ${currentNiche}`,
|
|
764
825
|
description: NICHE_CATEGORIES[targetNiche] || targetNiche,
|
|
765
826
|
};
|
|
766
827
|
console.error(
|
|
767
|
-
`[tag-service] 🔄 ${country} 强制方向切换: ${currentNiche}
|
|
828
|
+
`[tag-service] 🔄 ${country} 强制方向切换: ${currentNiche} -> ${targetNiche} (原因: 连续${health.consecutiveLowProductiveRounds}轮低效, 当前niche=${health.currentNiche || "?"})`,
|
|
768
829
|
);
|
|
769
830
|
}
|
|
770
831
|
|
|
@@ -778,16 +839,17 @@ export async function discoverTagsForCountry(
|
|
|
778
839
|
}
|
|
779
840
|
|
|
780
841
|
// ====== 自闭环 Step 3: 动态温度调整 ======
|
|
842
|
+
// 策略:默认 0.7,卡死时升温打破 LLM 确定性重复
|
|
781
843
|
let temperature = 0.7; // 默认
|
|
782
844
|
if (health) {
|
|
783
|
-
if (health.
|
|
784
|
-
temperature = 0
|
|
845
|
+
if (health.consecutiveLowProductiveRounds >= 3) {
|
|
846
|
+
temperature = 1.0; // 最大随机性,强制探索新空间
|
|
847
|
+
} else if (health.consecutiveLowProductiveRounds >= 2) {
|
|
848
|
+
temperature = 0.9; // 较高随机性,打破模式
|
|
849
|
+
} else if (health.healthStatus === "critical") {
|
|
850
|
+
temperature = 0.5; // 有问题但还没卡死,略保守
|
|
785
851
|
} else if (health.healthStatus === "warning") {
|
|
786
|
-
temperature = 0.
|
|
787
|
-
}
|
|
788
|
-
// 连续低效轮时进一步降温,让 LLM 更保守
|
|
789
|
-
if (health.consecutiveLowProductiveRounds >= 2) {
|
|
790
|
-
temperature = Math.min(temperature, 0.3);
|
|
852
|
+
temperature = 0.6;
|
|
791
853
|
}
|
|
792
854
|
}
|
|
793
855
|
|
|
@@ -903,6 +965,7 @@ export async function discoverTagsForCountry(
|
|
|
903
965
|
country,
|
|
904
966
|
round,
|
|
905
967
|
strategy,
|
|
968
|
+
niche: niche || forceNicheSwitch?.to || null,
|
|
906
969
|
tagsRaw: deduped,
|
|
907
970
|
tagsGenerated: unique,
|
|
908
971
|
tagsAdded: inserted.length,
|
|
@@ -1343,12 +1406,7 @@ export async function reflectAndLearn(store, country) {
|
|
|
1343
1406
|
console.error(` [${rule.ruleType}] ${rule.ruleText}`);
|
|
1344
1407
|
}
|
|
1345
1408
|
|
|
1346
|
-
// Step 6:
|
|
1347
|
-
const supersedeResult = supersedeRules(country, "new reflection round");
|
|
1348
|
-
console.error(
|
|
1349
|
-
`[reflection] ${country} 废弃 ${supersedeResult.superseded} 条旧规则`,
|
|
1350
|
-
);
|
|
1351
|
-
|
|
1409
|
+
// Step 6: 废弃旧规则(带相似度去重),写入新规则
|
|
1352
1410
|
const rulesToInsert = reflection.rules.map((r) => ({
|
|
1353
1411
|
country,
|
|
1354
1412
|
ruleType: r.ruleType,
|
|
@@ -1359,6 +1417,26 @@ export async function reflectAndLearn(store, country) {
|
|
|
1359
1417
|
analysisSummary: reflection.analysisSummary,
|
|
1360
1418
|
}));
|
|
1361
1419
|
|
|
1420
|
+
const supersedeResult = supersedeRules(
|
|
1421
|
+
country,
|
|
1422
|
+
rulesToInsert,
|
|
1423
|
+
"new reflection round",
|
|
1424
|
+
);
|
|
1425
|
+
if (supersedeResult.skipped) {
|
|
1426
|
+
console.error(
|
|
1427
|
+
`[reflection] ${country} 跳过反思(规则与上次高度相似),不写入新规则`,
|
|
1428
|
+
);
|
|
1429
|
+
return {
|
|
1430
|
+
reflected: true,
|
|
1431
|
+
rulesAdded: 0,
|
|
1432
|
+
analysis: reflection.analysisSummary,
|
|
1433
|
+
skipped: "rules too similar to previous reflection",
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
console.error(
|
|
1437
|
+
`[reflection] ${country} 废弃 ${supersedeResult.superseded} 条旧规则`,
|
|
1438
|
+
);
|
|
1439
|
+
|
|
1362
1440
|
const insertResult = insertReflectionRules(rulesToInsert);
|
|
1363
1441
|
console.error(
|
|
1364
1442
|
`[reflection] ${country} 写入 ${insertResult.inserted} 条新规则`,
|