@mazjq/zhiro 0.1.2 → 0.2.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.
- package/README.md +136 -64
- package/README.zh-CN.md +97 -41
- package/agent-skills/zhiro/SKILL.md +8 -2
- package/dist/acp-bridge.js +6 -6
- package/dist/{chunk-YJNBYSVM.js → chunk-2XJ45GUM.js} +141 -84
- package/dist/{chunk-5KCN32HQ.js → chunk-4EPNDXRS.js} +25 -0
- package/dist/{chunk-YYNUPNBH.js → chunk-4UWMDKMH.js} +142 -11
- package/dist/{chunk-F6DLONN7.js → chunk-7IV2NG2D.js} +2 -2
- package/dist/{chunk-J2D7DYGW.js → chunk-A722HITH.js} +2 -2
- package/dist/{chunk-LUKCNS5M.js → chunk-GRZ4VMSO.js} +1 -1
- package/dist/{chunk-NNH4ZG6F.js → chunk-H3EFVYGD.js} +1 -1
- package/dist/{chunk-UMCKC7XP.js → chunk-VOUILWMI.js} +36 -7
- package/dist/{chunk-LH25QWMP.js → chunk-ZKD2BKN3.js} +9 -5
- package/dist/{chunk-ZL2RKXOF.js → chunk-ZUUYRVX6.js} +1 -1
- package/dist/{cli-WO7FHPTE.js → cli-LUWVZ5NG.js} +3 -3
- package/dist/{login-2WAH5KPP.js → login-W5QYFWTY.js} +4 -4
- package/dist/{main-KCG27VLD.js → main-FTWFKWZH.js} +701 -135
- package/dist/research-data.js +7 -7
- package/dist/{vault-7KJ34T7Q.js → vault-JUTREQJK.js} +3 -3
- package/dist/zhiro.js +5 -5
- package/package.json +2 -2
- package/ui/assets/index-B7GrU8uc.js +39 -0
- package/ui/assets/index-Dt8b-A97.css +2 -0
- package/ui/index.html +2 -2
- package/ui/assets/index-DUzC3BlF.js +0 -11
- package/ui/assets/index-y_0yp8U5.css +0 -2
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
callData,
|
|
13
13
|
checkPoints,
|
|
14
14
|
codexSpec,
|
|
15
|
+
companyFinancials,
|
|
15
16
|
dataFile,
|
|
16
17
|
describeBackground,
|
|
17
18
|
docxHtml,
|
|
@@ -21,7 +22,6 @@ import {
|
|
|
21
22
|
extractNotice,
|
|
22
23
|
extractText,
|
|
23
24
|
fetchArticle,
|
|
24
|
-
getIntel,
|
|
25
25
|
getIntelDetail,
|
|
26
26
|
importSme,
|
|
27
27
|
keepData,
|
|
@@ -29,34 +29,41 @@ import {
|
|
|
29
29
|
loadImages,
|
|
30
30
|
loadSources,
|
|
31
31
|
mergeFacts,
|
|
32
|
+
normName,
|
|
32
33
|
noticeToListings,
|
|
33
34
|
oneshot,
|
|
34
35
|
readData,
|
|
36
|
+
readIntel,
|
|
35
37
|
readRun,
|
|
36
38
|
readZipEntry,
|
|
37
39
|
reextract,
|
|
38
40
|
refIndex,
|
|
39
41
|
refreshIntel,
|
|
42
|
+
resolveNeeq,
|
|
40
43
|
resumePending,
|
|
41
44
|
scanFolder,
|
|
45
|
+
smeMatch,
|
|
42
46
|
smeRun,
|
|
43
47
|
spawnRun,
|
|
44
48
|
startSmeResearch,
|
|
45
49
|
tools,
|
|
46
50
|
workerStatus
|
|
47
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-2XJ45GUM.js";
|
|
48
52
|
import {
|
|
49
53
|
dshSpec
|
|
50
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-ZUUYRVX6.js";
|
|
51
55
|
import {
|
|
52
56
|
FACT_KINDS,
|
|
53
57
|
SEP,
|
|
54
58
|
SKILL_FILE,
|
|
55
59
|
UI_DIST,
|
|
60
|
+
chooseModel,
|
|
56
61
|
claudeSpec,
|
|
57
62
|
cliBackend,
|
|
58
63
|
factLabels,
|
|
59
64
|
followedModel,
|
|
65
|
+
harnessModel,
|
|
66
|
+
harnessModels,
|
|
60
67
|
listActions,
|
|
61
68
|
listFacts,
|
|
62
69
|
listPresets,
|
|
@@ -82,7 +89,7 @@ import {
|
|
|
82
89
|
undoAction,
|
|
83
90
|
undoPlan,
|
|
84
91
|
upsertFact
|
|
85
|
-
} from "./chunk-
|
|
92
|
+
} from "./chunk-VOUILWMI.js";
|
|
86
93
|
import "./chunk-OOHBM3BT.js";
|
|
87
94
|
import {
|
|
88
95
|
UpgradeFailed,
|
|
@@ -92,13 +99,13 @@ import {
|
|
|
92
99
|
stampApp,
|
|
93
100
|
upgradeVault,
|
|
94
101
|
zipRead
|
|
95
|
-
} from "./chunk-
|
|
102
|
+
} from "./chunk-A722HITH.js";
|
|
96
103
|
import {
|
|
97
104
|
VERSION
|
|
98
|
-
} from "./chunk-
|
|
105
|
+
} from "./chunk-H3EFVYGD.js";
|
|
99
106
|
import {
|
|
100
107
|
WorkflowEngine
|
|
101
|
-
} from "./chunk-
|
|
108
|
+
} from "./chunk-GRZ4VMSO.js";
|
|
102
109
|
import {
|
|
103
110
|
asAgentCall,
|
|
104
111
|
createLlm,
|
|
@@ -109,7 +116,7 @@ import {
|
|
|
109
116
|
replyJob,
|
|
110
117
|
usageSummary,
|
|
111
118
|
withPurpose
|
|
112
|
-
} from "./chunk-
|
|
119
|
+
} from "./chunk-7IV2NG2D.js";
|
|
113
120
|
import {
|
|
114
121
|
Domain,
|
|
115
122
|
DomainError,
|
|
@@ -131,7 +138,7 @@ import {
|
|
|
131
138
|
scanHistory,
|
|
132
139
|
searchCompanies,
|
|
133
140
|
setOverride
|
|
134
|
-
} from "./chunk-
|
|
141
|
+
} from "./chunk-ZKD2BKN3.js";
|
|
135
142
|
import {
|
|
136
143
|
SYNC_REASONS,
|
|
137
144
|
addSource,
|
|
@@ -163,14 +170,14 @@ import {
|
|
|
163
170
|
sourceScan,
|
|
164
171
|
sources,
|
|
165
172
|
splitList
|
|
166
|
-
} from "./chunk-
|
|
173
|
+
} from "./chunk-4UWMDKMH.js";
|
|
167
174
|
import {
|
|
168
175
|
SchemaMismatch,
|
|
169
176
|
json,
|
|
170
177
|
now,
|
|
171
178
|
openDb,
|
|
172
179
|
tx
|
|
173
|
-
} from "./chunk-
|
|
180
|
+
} from "./chunk-4EPNDXRS.js";
|
|
174
181
|
import {
|
|
175
182
|
clearConfigTemps,
|
|
176
183
|
configFile,
|
|
@@ -492,16 +499,16 @@ function scheduleAllDeadlines(s) {
|
|
|
492
499
|
const db2 = s.domain.db;
|
|
493
500
|
const ids = db2.prepare(`select p.id from position p where ${HELD}`).all().map((r) => r.id);
|
|
494
501
|
const resolved = resolveFor(db2, ids);
|
|
495
|
-
const
|
|
502
|
+
const keep2 = /* @__PURE__ */ new Set();
|
|
496
503
|
for (const id of ids) {
|
|
497
504
|
const r = resolved.get(id);
|
|
498
505
|
if (!r || r.expired || !r.date && !asap(r)) continue;
|
|
499
506
|
const key = `deadline_reminder:${id}:${r.date ?? "asap"}`;
|
|
500
|
-
|
|
507
|
+
keep2.add(key);
|
|
501
508
|
s.workflows.start("deadline_reminder", id, key);
|
|
502
509
|
}
|
|
503
|
-
for (const w of db2.prepare("select id, idempotency_key k from workflow where kind = 'deadline_reminder' and state = 'running'").all()) if (!
|
|
504
|
-
return
|
|
510
|
+
for (const w of db2.prepare("select id, idempotency_key k from workflow where kind = 'deadline_reminder' and state = 'running'").all()) if (!keep2.has(w.k)) db2.prepare("delete from workflow where id = ?").run(w.id);
|
|
511
|
+
return keep2.size;
|
|
505
512
|
}
|
|
506
513
|
__name(scheduleAllDeadlines, "scheduleAllDeadlines");
|
|
507
514
|
|
|
@@ -509,6 +516,515 @@ __name(scheduleAllDeadlines, "scheduleAllDeadlines");
|
|
|
509
516
|
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
510
517
|
import { join as join3 } from "node:path";
|
|
511
518
|
import { ulid } from "ulid";
|
|
519
|
+
|
|
520
|
+
// apps/daemon/src/service/employer.ts
|
|
521
|
+
var UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/128.0 Safari/537.36";
|
|
522
|
+
var F10 = "https://datacenter.eastmoney.com/securities/api/data/v1/get";
|
|
523
|
+
var A_SHARE = ["深交所主板A股", "上交所主板A股", "深交所创业板A股", "上交所科创板A股", "北京证券交易所A股", "深交所风险警示板A股", "上交所风险警示板A股"];
|
|
524
|
+
var KEEP_DAYS = 7;
|
|
525
|
+
var sleep = /* @__PURE__ */ __name((ms) => new Promise((r) => setTimeout(r, ms)), "sleep");
|
|
526
|
+
async function f10(report, columns, filter, page = 1) {
|
|
527
|
+
const url = `${F10}?reportName=${report}&columns=${columns}&filter=${encodeURIComponent(filter)}&pageNumber=${page}&pageSize=500&source=HSF10&client=PC`;
|
|
528
|
+
for (let k = 0; ; k++) {
|
|
529
|
+
try {
|
|
530
|
+
const res = await fetch(url, { headers: { "user-agent": UA }, signal: AbortSignal.timeout(2e4) });
|
|
531
|
+
const x = await res.json();
|
|
532
|
+
if (!x?.success && x?.code !== 9201) throw new Error(`东方财富 ${report}:${x?.message ?? res.status}`);
|
|
533
|
+
return { data: x?.result?.data ?? [], pages: x?.result?.pages ?? 0 };
|
|
534
|
+
} catch (e) {
|
|
535
|
+
if (k >= 1) throw e;
|
|
536
|
+
await sleep(1500);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
__name(f10, "f10");
|
|
541
|
+
async function f10All(report, columns, filter) {
|
|
542
|
+
const first = await f10(report, columns, filter);
|
|
543
|
+
const out = [...first.data];
|
|
544
|
+
for (let p = 2; p <= first.pages; p++) {
|
|
545
|
+
await sleep(300);
|
|
546
|
+
out.push(...(await f10(report, columns, filter, p)).data);
|
|
547
|
+
}
|
|
548
|
+
return out;
|
|
549
|
+
}
|
|
550
|
+
__name(f10All, "f10All");
|
|
551
|
+
function cached(db2, key, now3) {
|
|
552
|
+
const r = db2.prepare("select fetched_at, data_json from peer_snapshot where key = ?").get(key);
|
|
553
|
+
if (!r || now3 - Date.parse(r.fetched_at) > KEEP_DAYS * 864e5) return null;
|
|
554
|
+
return { at: r.fetched_at, data: JSON.parse(r.data_json) };
|
|
555
|
+
}
|
|
556
|
+
__name(cached, "cached");
|
|
557
|
+
var keep = /* @__PURE__ */ __name((db2, key, data, now3) => db2.prepare("insert or replace into peer_snapshot(key, fetched_at, data_json) values (?,?,?)").run(key, new Date(now3).toISOString(), JSON.stringify(data)), "keep");
|
|
558
|
+
async function remembered(db2, key, now3, look) {
|
|
559
|
+
const r = db2.prepare("select fetched_at, data_json from peer_snapshot where key = ?").get(key);
|
|
560
|
+
if (r && now3 - Date.parse(r.fetched_at) <= KEEP_DAYS * 864e5) return JSON.parse(r.data_json).v;
|
|
561
|
+
const v = await look();
|
|
562
|
+
keep(db2, key, { v }, now3);
|
|
563
|
+
return v;
|
|
564
|
+
}
|
|
565
|
+
__name(remembered, "remembered");
|
|
566
|
+
async function universe(db2, now3 = Date.now()) {
|
|
567
|
+
const hit = cached(db2, "universe:3", now3);
|
|
568
|
+
if (hit) return hit;
|
|
569
|
+
const rows = await f10All("RPT_F10_BASIC_ORGINFO", "SECUCODE,SECURITY_NAME_ABBR,ORG_NAME,BOARD_NAME_LEVEL,EMP_NUM,INDUSTRYCSRC1", `(SECURITY_TYPE in (${A_SHARE.map((t) => `"${t}"`).join(",")}))`);
|
|
570
|
+
const data = rows.filter((r) => r.BOARD_NAME_LEVEL).map((r) => ({ code: r.SECUCODE, name: r.SECURITY_NAME_ABBR ?? "", org: r.ORG_NAME ?? "", industry: r.BOARD_NAME_LEVEL, employees: Number(r.EMP_NUM) > 0 ? Number(r.EMP_NUM) : null, csrc: r.INDUSTRYCSRC1 ?? "" }));
|
|
571
|
+
keep(db2, "universe:3", data, now3);
|
|
572
|
+
return { at: new Date(now3).toISOString(), data };
|
|
573
|
+
}
|
|
574
|
+
__name(universe, "universe");
|
|
575
|
+
function latestYear(now3 = Date.now()) {
|
|
576
|
+
const d = new Date(now3 + 8 * 36e5);
|
|
577
|
+
return d.getUTCMonth() >= 4 ? d.getUTCFullYear() - 1 : d.getUTCFullYear() - 2;
|
|
578
|
+
}
|
|
579
|
+
__name(latestYear, "latestYear");
|
|
580
|
+
var sector = /* @__PURE__ */ __name((industry) => industry.split("-")[0], "sector");
|
|
581
|
+
function peerGroup(c) {
|
|
582
|
+
if (c.peerSw) return { key: c.peerSw, label: `申万一级「${c.peerSw}」`, has: /* @__PURE__ */ __name((x) => sector(x.industry) === c.peerSw, "has") };
|
|
583
|
+
if (c.market === "新三板" && c.csrc) return { key: `csrc:${c.csrc}`, label: `证监会行业「${c.csrc}」`, has: /* @__PURE__ */ __name((x) => x.csrc === c.csrc, "has") };
|
|
584
|
+
const top = sector(c.industry);
|
|
585
|
+
return { key: top, label: `申万一级「${top}」`, has: /* @__PURE__ */ __name((x) => sector(x.industry) === top, "has") };
|
|
586
|
+
}
|
|
587
|
+
__name(peerGroup, "peerGroup");
|
|
588
|
+
async function industryPool(db2, group, year, all, now3 = Date.now()) {
|
|
589
|
+
const key = `industry:${group.key}:${year}`;
|
|
590
|
+
const hit = cached(db2, key, now3);
|
|
591
|
+
if (hit) return hit;
|
|
592
|
+
const members = all.filter(group.has);
|
|
593
|
+
const rows = new Map(members.map((c) => [c.code, { ...c, years: {} }]));
|
|
594
|
+
await statements(rows, year);
|
|
595
|
+
const data = [...rows.values()].filter((r) => (r.years[String(year)]?.revenue ?? 0) > 0);
|
|
596
|
+
keep(db2, key, data, now3);
|
|
597
|
+
return { at: new Date(now3).toISOString(), data };
|
|
598
|
+
}
|
|
599
|
+
__name(industryPool, "industryPool");
|
|
600
|
+
async function statements(rows, year) {
|
|
601
|
+
const members = [...rows.values()];
|
|
602
|
+
const dates = [year, year - 1, year - 2, year - 3].map((y) => `'${y}-12-31'`).join(",");
|
|
603
|
+
const row2 = /* @__PURE__ */ __name((code, date) => {
|
|
604
|
+
const r = rows.get(code), y = date.slice(0, 4);
|
|
605
|
+
return r.years[y] ??= { revenue: null, netProfit: null, rd: null, opCost: null, staffCash: null, dividendCash: null, capex: null };
|
|
606
|
+
}, "row");
|
|
607
|
+
const num = /* @__PURE__ */ __name((v) => v === null || v === void 0 || v === "" || !Number.isFinite(Number(v)) ? null : Number(v), "num");
|
|
608
|
+
for (let i = 0; i < members.length; i += 100) {
|
|
609
|
+
const codes = `(SECUCODE in (${members.slice(i, i + 100).map((c) => `"${c.code}"`).join(",")}))(REPORT_DATE in (${dates}))`;
|
|
610
|
+
for (const r of await f10All("RPT_F10_FINANCE_GINCOME", "SECUCODE,REPORT_DATE,TOTAL_OPERATE_INCOME,PARENT_NETPROFIT,RESEARCH_EXPENSE,OPERATE_COST", codes)) {
|
|
611
|
+
if (!rows.has(r.SECUCODE)) continue;
|
|
612
|
+
Object.assign(row2(r.SECUCODE, r.REPORT_DATE), { revenue: num(r.TOTAL_OPERATE_INCOME), netProfit: num(r.PARENT_NETPROFIT), rd: num(r.RESEARCH_EXPENSE), opCost: num(r.OPERATE_COST) });
|
|
613
|
+
}
|
|
614
|
+
await sleep(300);
|
|
615
|
+
for (const r of await f10All("RPT_F10_FINANCE_GCASHFLOW", "SECUCODE,REPORT_DATE,PAY_STAFF_CASH,ASSIGN_DIVIDEND_PORFIT,CONSTRUCT_LONG_ASSET", codes)) {
|
|
616
|
+
if (!rows.has(r.SECUCODE)) continue;
|
|
617
|
+
Object.assign(row2(r.SECUCODE, r.REPORT_DATE), { staffCash: num(r.PAY_STAFF_CASH), dividendCash: num(r.ASSIGN_DIVIDEND_PORFIT), capex: num(r.CONSTRUCT_LONG_ASSET) });
|
|
618
|
+
}
|
|
619
|
+
await sleep(300);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
__name(statements, "statements");
|
|
623
|
+
async function neeqCompany(name) {
|
|
624
|
+
const hit = await resolveNeeq(name, normName(name));
|
|
625
|
+
if (!hit) return null;
|
|
626
|
+
const r = (await f10("RPT_F10_BASIC_ORGINFO", "SECUCODE,SECURITY_NAME_ABBR,ORG_NAME,BOARD_NAME_LEVEL,EMP_NUM,INDUSTRYCSRC1", `(SECUCODE="${hit.code}.NQ")`)).data[0];
|
|
627
|
+
return r?.BOARD_NAME_LEVEL ? { code: r.SECUCODE, name: r.SECURITY_NAME_ABBR ?? hit.name, org: r.ORG_NAME ?? "", industry: r.BOARD_NAME_LEVEL, employees: Number(r.EMP_NUM) > 0 ? Number(r.EMP_NUM) : null, market: "新三板", csrc: r.INDUSTRYCSRC1 ?? "" } : null;
|
|
628
|
+
}
|
|
629
|
+
__name(neeqCompany, "neeqCompany");
|
|
630
|
+
async function overseas(name) {
|
|
631
|
+
const f = await companyFinancials({ name });
|
|
632
|
+
if (f.status === "failed") throw new Error(`${name}:认出了上市公司但数据没取成`);
|
|
633
|
+
const sec = f.status === "listed" ? f.security : null;
|
|
634
|
+
if (!sec || sec.market !== "港股" && sec.market !== "美股") return null;
|
|
635
|
+
const hk = sec.market === "港股", code = hk ? `${sec.code}.HK` : sec.code;
|
|
636
|
+
const main = (await f10(hk ? "RPT_HKF10_FN_MAININDICATOR" : "RPT_USF10_FN_GMAININDICATOR", "ALL", `(SECUCODE="${code}")`)).data;
|
|
637
|
+
const annual = main.filter((r) => hk ? /年报/.test(String(r.REPORT_TYPE)) : r.DATE_TYPE === "年报");
|
|
638
|
+
await sleep(300);
|
|
639
|
+
const rd = (await f10(hk ? "RPT_HKF10_FN_INCOME_PC" : "RPT_USF10_FN_INCOME", "ALL", hk ? `(SECUCODE="${code}")(STD_ITEM_NAME="研发费用")(DATE_TYPE_CODE="001")` : `(SECUCODE="${code}")(REPORT_TYPE="年报")(ITEM_NAME="研发费用")`).catch(() => ({ data: [] }))).data;
|
|
640
|
+
const rdOn = new Map(rd.map((r) => [String(r.REPORT_DATE).slice(0, 10), Number(r.AMOUNT)]));
|
|
641
|
+
const years = {};
|
|
642
|
+
for (const r of annual) {
|
|
643
|
+
const y = String(hk ? r.REPORT_TYPE : r.REPORT_DATA_TYPE).match(/\d{4}/)?.[0];
|
|
644
|
+
if (!y || years[y]) continue;
|
|
645
|
+
const num = /* @__PURE__ */ __name((v) => v === null || v === void 0 || v === "" || !Number.isFinite(Number(v)) ? null : Number(v), "num");
|
|
646
|
+
const revenue = num(r.OPERATE_INCOME), gm = num(r.GROSS_PROFIT_RATIO);
|
|
647
|
+
years[y] = { revenue, netProfit: num(hk ? r.HOLDER_PROFIT : r.PARENT_HOLDER_NETPROFIT), rd: rdOn.get(String(r.REPORT_DATE).slice(0, 10)) ?? null, opCost: revenue !== null && gm !== null ? revenue * (1 - gm / 100) : null, staffCash: null, dividendCash: null, capex: null };
|
|
648
|
+
}
|
|
649
|
+
return { code, name: sec.name, org: sec.name, industry: f.basic?.industry ?? "", employees: f.basic?.employees ?? null, market: sec.market, approx: !!f.approx, years };
|
|
650
|
+
}
|
|
651
|
+
__name(overseas, "overseas");
|
|
652
|
+
function unlistedFacts(db2, name) {
|
|
653
|
+
const n = normName(name);
|
|
654
|
+
const boss = n.length >= 2 ? db2.prepare("select brand, stage, scale, industry, last_seen from boss_candidate order by last_seen desc limit 2000").all().find((b) => b.brand && normName(b.brand) === n) : void 0;
|
|
655
|
+
const sme = smeMatch(db2, name).matches[0];
|
|
656
|
+
const co = db2.prepare("select id from company where name = ? or norm = ?").get(name, n);
|
|
657
|
+
const ipo = co ? db2.prepare("select data_json, fetched_at from company_intel where company_id = ? and kind = 'ipo'").get(co.id) : void 0;
|
|
658
|
+
const szse = ipo ? JSON.parse(ipo.data_json)?.szse : null;
|
|
659
|
+
return {
|
|
660
|
+
...boss ? { boss: { stage: boss.stage, scale: boss.scale, industry: boss.industry, seen: String(boss.last_seen).slice(0, 10) } } : {},
|
|
661
|
+
...sme ? { sme: sme.level } : {},
|
|
662
|
+
...szse ? { ipo: { board: "深交所", stage: szse.stage, date: szse.updatedAt || szse.acceptedAt || "" } } : {}
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
__name(unlistedFacts, "unlistedFacts");
|
|
666
|
+
var FACETS = [
|
|
667
|
+
{ key: "growth", label: "成长", measure: "营收三年复合增速" },
|
|
668
|
+
{ key: "rd", label: "研发", measure: "研发费用占营收" },
|
|
669
|
+
{ key: "pay", label: "待遇信号", measure: "每名员工对应的职工现金支出", note: "粗指标:含奖金、社保、离职补偿,员工数取最新" },
|
|
670
|
+
{ key: "profit", label: "盈利", measure: "归母净利润占营收" }
|
|
671
|
+
];
|
|
672
|
+
var ratio = /* @__PURE__ */ __name((a, b) => a === null || a === void 0 || !b || b <= 0 ? null : a / b, "ratio");
|
|
673
|
+
var change = /* @__PURE__ */ __name((a, b) => a === null || a === void 0 || b === null || b === void 0 || b === 0 ? null : (a - b) / Math.abs(b), "change");
|
|
674
|
+
function measures(r, year) {
|
|
675
|
+
const y = r.years[String(year)] ?? {}, p = r.years[String(year - 1)] ?? {}, b = r.years[String(year - 3)];
|
|
676
|
+
const growth = b && (b.revenue ?? 0) > 0 && (y.revenue ?? 0) > 0 ? (y.revenue / b.revenue) ** (1 / 3) - 1 : null;
|
|
677
|
+
return {
|
|
678
|
+
main: { growth, rd: ratio(y.rd, y.revenue), pay: ratio(y.staffCash, r.employees), profit: ratio(y.netProfit, y.revenue) },
|
|
679
|
+
more: {
|
|
680
|
+
revenue: y.revenue ?? null,
|
|
681
|
+
netProfit: y.netProfit ?? null,
|
|
682
|
+
prevNetProfit: p.netProfit ?? null,
|
|
683
|
+
employees: r.employees,
|
|
684
|
+
revenueChange: change(y.revenue, p.revenue),
|
|
685
|
+
profitChange: change(y.netProfit, p.netProfit),
|
|
686
|
+
staffCashChange: change(y.staffCash, p.staffCash),
|
|
687
|
+
rdChange: change(y.rd, p.rd),
|
|
688
|
+
grossMargin: y.revenue && y.opCost !== null && y.opCost !== void 0 ? (y.revenue - y.opCost) / y.revenue : null,
|
|
689
|
+
capexToRevenue: ratio(y.capex, y.revenue),
|
|
690
|
+
dividendCashToProfit: (y.netProfit ?? 0) > 0 ? ratio(y.dividendCash, y.netProfit) : null
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
__name(measures, "measures");
|
|
695
|
+
function percentile(values, v) {
|
|
696
|
+
let below = 0, same2 = 0;
|
|
697
|
+
for (const x of values) {
|
|
698
|
+
if (x < v) below++;
|
|
699
|
+
else if (x === v) same2++;
|
|
700
|
+
}
|
|
701
|
+
return values.length ? Math.round((below + same2 / 2) / values.length * 100) : null;
|
|
702
|
+
}
|
|
703
|
+
__name(percentile, "percentile");
|
|
704
|
+
function moved(m) {
|
|
705
|
+
const out = [], up = /* @__PURE__ */ __name((x) => x !== null && x > 0.02, "up"), down = /* @__PURE__ */ __name((x) => x !== null && x < -0.02, "down");
|
|
706
|
+
const turned = m.prevNetProfit !== null && m.netProfit !== null ? m.prevNetProfit < 0 && m.netProfit > 0 ? "扭亏为盈" : m.prevNetProfit > 0 && m.netProfit < 0 ? "由盈转亏" : null : null;
|
|
707
|
+
if (turned) out.push(`营收${up(m.revenueChange) ? "增长" : down(m.revenueChange) ? "下降" : "基本持平"},${turned}`);
|
|
708
|
+
else if (up(m.revenueChange) && up(m.profitChange)) out.push("营收和利润同时增长");
|
|
709
|
+
else if (up(m.revenueChange) && down(m.profitChange)) out.push("营收增长、利润下降(增收不增利)");
|
|
710
|
+
else if (down(m.revenueChange) && up(m.profitChange)) out.push(`营收下降、利润增长${down(m.staffCashChange) || down(m.rdChange) ? `;同期${[down(m.staffCashChange) && "职工现金", down(m.rdChange) && "研发费用"].filter(Boolean).join("、")}减少,利润可能来自压缩成本(待核实)` : ""}`);
|
|
711
|
+
else if (down(m.revenueChange) && down(m.profitChange)) out.push("营收和利润同时下降");
|
|
712
|
+
if (up(m.profitChange) && down(m.staffCashChange) && !down(m.revenueChange)) out.push("利润增长的同时职工现金减少(可能减员或降薪,待核实)");
|
|
713
|
+
if (m.netProfit !== null && m.netProfit < 0 && turned !== "由盈转亏") out.push("当年亏损");
|
|
714
|
+
return out;
|
|
715
|
+
}
|
|
716
|
+
__name(moved, "moved");
|
|
717
|
+
function labels(pct) {
|
|
718
|
+
const out = [], at = /* @__PURE__ */ __name((k, n) => (pct[k] ?? -1) >= n, "at");
|
|
719
|
+
if (at("rd", 75) && at("growth", 50)) out.push({ label: "研发驱动", rule: "研发百分位 ≥ 75 且成长 ≥ 50" });
|
|
720
|
+
else if (at("growth", 75)) out.push({ label: "高成长", rule: "成长百分位 ≥ 75" });
|
|
721
|
+
if (at("profit", 75) && pct.growth !== null && pct.growth < 50) out.push({ label: "稳健盈利", rule: "盈利百分位 ≥ 75 且成长 < 50" });
|
|
722
|
+
if (at("pay", 75)) out.push({ label: "职工现金支出靠前", rule: "待遇信号百分位 ≥ 75" });
|
|
723
|
+
return out;
|
|
724
|
+
}
|
|
725
|
+
__name(labels, "labels");
|
|
726
|
+
var SCENARIOS = {
|
|
727
|
+
成长研发优先: { growth: 0.35, rd: 0.35, pay: 0.15, profit: 0.15 },
|
|
728
|
+
均衡: { growth: 0.25, rd: 0.25, pay: 0.25, profit: 0.25 },
|
|
729
|
+
稳定回报优先: { growth: 0.15, rd: 0.15, pay: 0.35, profit: 0.35 }
|
|
730
|
+
};
|
|
731
|
+
var KEYS = FACETS.map((f) => f.key);
|
|
732
|
+
function rng(seed) {
|
|
733
|
+
let a = seed >>> 0;
|
|
734
|
+
return () => {
|
|
735
|
+
a = a + 1831565813 >>> 0;
|
|
736
|
+
let t = a;
|
|
737
|
+
t = Math.imul(t ^ t >>> 15, t | 1);
|
|
738
|
+
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
|
739
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
__name(rng, "rng");
|
|
743
|
+
function sampleWeights(center, spread, n, seed = 1) {
|
|
744
|
+
const r = rng(seed), out = [];
|
|
745
|
+
for (let tries = 0; out.length < n && tries < n * 2e3; tries++) {
|
|
746
|
+
const e = KEYS.map(() => -Math.log(1 - r())), s = e.reduce((a, b) => a + b, 0);
|
|
747
|
+
const w = Object.fromEntries(KEYS.map((k, i) => [k, e[i] / s]));
|
|
748
|
+
if (KEYS.every((k) => Math.abs(w[k] - center[k]) <= spread)) out.push(w);
|
|
749
|
+
}
|
|
750
|
+
return out;
|
|
751
|
+
}
|
|
752
|
+
__name(sampleWeights, "sampleWeights");
|
|
753
|
+
var score = /* @__PURE__ */ __name((p, w, fill) => KEYS.reduce((a, k) => a + w[k] * (p[k] ?? fill ?? 0), 0), "score");
|
|
754
|
+
var rankOf = /* @__PURE__ */ __name((mine, others) => 1 + others.filter((x) => x > mine).length, "rankOf");
|
|
755
|
+
var q = /* @__PURE__ */ __name((xs, f) => {
|
|
756
|
+
const s = [...xs].sort((a, b) => a - b);
|
|
757
|
+
return s[Math.min(s.length - 1, Math.floor(f * s.length))];
|
|
758
|
+
}, "q");
|
|
759
|
+
function placeUnder(picks, center, spread, n = 2e3, seed = 1) {
|
|
760
|
+
const complete = picks.filter((p) => KEYS.every((k) => p.pct[k] !== null));
|
|
761
|
+
const samples = sampleWeights(center, spread, n, seed);
|
|
762
|
+
const ranks = new Map(complete.map((p) => [p.code, []]));
|
|
763
|
+
for (const w of samples) {
|
|
764
|
+
const s = complete.map((p) => score(p.pct, w));
|
|
765
|
+
complete.forEach((p, i) => ranks.get(p.code).push(rankOf(s[i], s.filter((_, j) => j !== i))));
|
|
766
|
+
}
|
|
767
|
+
const at = complete.map((p) => score(p.pct, center));
|
|
768
|
+
const out = /* @__PURE__ */ new Map();
|
|
769
|
+
complete.forEach((p, i) => {
|
|
770
|
+
const r = ranks.get(p.code);
|
|
771
|
+
out.set(p.code, { place: rankOf(at[i], at.filter((_, j) => j !== i)), usually: [q(r, 0.1), q(r, 0.9)], top3Share: r.length && complete.length > 3 ? Math.round(r.filter((x) => x <= 3).length / r.length * 20) * 5 : null });
|
|
772
|
+
});
|
|
773
|
+
for (const p of picks.filter((x) => !complete.includes(x))) {
|
|
774
|
+
const known = KEYS.filter((k) => p.pct[k] !== null), wsum = known.reduce((a, k) => a + center[k], 0);
|
|
775
|
+
const cond = wsum ? known.reduce((a, k) => a + center[k] / wsum * p.pct[k], 0) : null;
|
|
776
|
+
out.set(p.code, {
|
|
777
|
+
place: null,
|
|
778
|
+
byKnown: cond === null ? null : rankOf(cond, at),
|
|
779
|
+
best: rankOf(score(p.pct, center, 100), at),
|
|
780
|
+
worst: rankOf(score(p.pct, center, 0), at),
|
|
781
|
+
missing: KEYS.filter((k) => p.pct[k] === null)
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
return { samples: samples.length, places: out };
|
|
785
|
+
}
|
|
786
|
+
__name(placeUnder, "placeUnder");
|
|
787
|
+
function notOutdone(picks) {
|
|
788
|
+
const complete = picks.filter((p) => KEYS.every((k) => p.pct[k] !== null));
|
|
789
|
+
const beats = /* @__PURE__ */ __name((a, b) => KEYS.every((k) => a.pct[k] >= b.pct[k]) && KEYS.some((k) => a.pct[k] > b.pct[k]), "beats");
|
|
790
|
+
return new Map(complete.map((p) => [p.code, complete.filter((o) => o !== p && beats(o, p)).map((o) => o.code)]));
|
|
791
|
+
}
|
|
792
|
+
__name(notOutdone, "notOutdone");
|
|
793
|
+
function findCompany(all, query) {
|
|
794
|
+
const s = query.trim();
|
|
795
|
+
const code = s.match(/^(\d{6})(\.(SZ|SH|BJ))?$/i);
|
|
796
|
+
if (code) return all.find((c) => c.code.startsWith(code[1])) ?? null;
|
|
797
|
+
const exact = all.filter((c) => c.name === s || c.org === s);
|
|
798
|
+
if (exact.length) return exact[0];
|
|
799
|
+
const n = normName(s);
|
|
800
|
+
const near = n.length >= 2 ? all.filter((c) => normName(c.org) === n || c.name.replace(/\s/g, "") === n || normName(c.org).includes(n)) : [];
|
|
801
|
+
return near.length === 1 ? near[0] : near.length ? { candidates: near.slice(0, 5) } : null;
|
|
802
|
+
}
|
|
803
|
+
__name(findCompany, "findCompany");
|
|
804
|
+
async function compare(db2, q2, now3 = Date.now()) {
|
|
805
|
+
const year = q2.year ?? latestYear(now3);
|
|
806
|
+
const uni = await universe(db2, now3);
|
|
807
|
+
const found = [], notFound = [];
|
|
808
|
+
const elsewhere = [];
|
|
809
|
+
for (const name of q2.companies.slice(0, 15)) {
|
|
810
|
+
const c = findCompany(uni.data, name) ?? await remembered(db2, `neeq:${name}`, now3, () => neeqCompany(name)).catch(() => null);
|
|
811
|
+
if (!c) elsewhere.push(name);
|
|
812
|
+
else if ("candidates" in c) notFound.push({ query: name, reason: "有几家名字相近,用股票代码再查", candidates: c.candidates.map((x) => `${x.name} ${x.code}`) });
|
|
813
|
+
else if (!found.some((f) => f.code === c.code)) found.push(c);
|
|
814
|
+
}
|
|
815
|
+
const tops = found.filter((c) => !c.market).map((c) => sector(c.industry));
|
|
816
|
+
const peerSw = q2.peerIndustry?.trim() || [...new Set(tops)].sort((a, b) => tops.filter((x) => x === b).length - tops.filter((x) => x === a).length)[0];
|
|
817
|
+
const abroad = /* @__PURE__ */ new Map();
|
|
818
|
+
for (const name of elsewhere) {
|
|
819
|
+
const r = await remembered(db2, `abroad:${name}`, now3, () => overseas(name)).catch(() => null);
|
|
820
|
+
if (!r) notFound.push({ query: name, reason: "不是 A 股、新三板、港股或美股上市公司(或名字写法不同);未上市公司没有公开财报,放不进这张表", known: unlistedFacts(db2, name) });
|
|
821
|
+
else if (!peerSw) notFound.push({ query: name, reason: `${r.market}公司没有申万行业:和 A 股公司一起比,或给出拿哪个申万一级行业当同行(peerIndustry)` });
|
|
822
|
+
else if (!uni.data.some((c) => sector(c.industry) === peerSw)) notFound.push({ query: name, reason: `没有叫「${peerSw}」的申万一级行业` });
|
|
823
|
+
else if (!found.some((f) => f.code === r.code)) {
|
|
824
|
+
r.peerSw = peerSw;
|
|
825
|
+
abroad.set(r.code, r);
|
|
826
|
+
found.push(r);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const pools = /* @__PURE__ */ new Map();
|
|
830
|
+
for (const c of found) {
|
|
831
|
+
const g = peerGroup(c);
|
|
832
|
+
if (!pools.has(g.label)) pools.set(g.label, await industryPool(db2, g, year, uni.data, now3));
|
|
833
|
+
}
|
|
834
|
+
const outside = new Map(found.filter((c) => c.market === "新三板").map((c) => [c.code, { ...c, years: {} }]));
|
|
835
|
+
if (outside.size) await statements(outside, year);
|
|
836
|
+
const rows = found.map((c) => {
|
|
837
|
+
const pool = pools.get(peerGroup(c).label);
|
|
838
|
+
const other = outside.get(c.code) ?? abroad.get(c.code);
|
|
839
|
+
const me = pool.data.find((r) => r.code === c.code) ?? (other?.years[String(year)]?.revenue ? other : void 0);
|
|
840
|
+
if (!me) return { company: c, missingYear: true };
|
|
841
|
+
const m = measures(me, year);
|
|
842
|
+
const all = pool.data.map((r) => measures(r, year).main);
|
|
843
|
+
const pct = Object.fromEntries(KEYS.map((k) => {
|
|
844
|
+
const v = m.main[k];
|
|
845
|
+
return [k, v === null ? null : percentile(all.map((x) => x[k]).filter((x) => x !== null), v)];
|
|
846
|
+
}));
|
|
847
|
+
const n = Object.fromEntries(KEYS.map((k) => [k, all.filter((x) => x[k] !== null).length]));
|
|
848
|
+
return { company: c, years: me.years, value: m.main, pct, peers: n, more: m.more, moved: moved(m.more), labels: labels(pct), coverage: KEYS.filter((k) => m.main[k] !== null).length };
|
|
849
|
+
});
|
|
850
|
+
const ok = rows.filter((r) => "pct" in r);
|
|
851
|
+
const picks = ok.map((r) => ({ code: r.company.code, pct: r.pct }));
|
|
852
|
+
const spread = Math.max(0.02, Math.min(1, q2.spread ?? 0.1));
|
|
853
|
+
const scenarios = { ...SCENARIOS };
|
|
854
|
+
if (q2.weights) {
|
|
855
|
+
const w = Object.fromEntries(KEYS.map((k) => [k, Math.max(0, q2.weights[k] ?? 0)])), s = KEYS.reduce((a, k) => a + w[k], 0);
|
|
856
|
+
if (s > 0) scenarios.你设的权重 = Object.fromEntries(KEYS.map((k) => [k, w[k] / s]));
|
|
857
|
+
}
|
|
858
|
+
const under = Object.fromEntries(Object.entries(scenarios).map(([name, w]) => [name, { weights: w, ...placeUnder(picks, w, spread) }]));
|
|
859
|
+
const any = placeUnder(picks, SCENARIOS.均衡, 1, 4e3, 2);
|
|
860
|
+
const outdone = notOutdone(picks);
|
|
861
|
+
return {
|
|
862
|
+
year,
|
|
863
|
+
peerGroup: "同一行业的全部 A 股上市公司(当年有营收的):A 股公司按申万一级,新三板公司按证监会行业",
|
|
864
|
+
data: { universeAt: uni.at, pools: Object.fromEntries([...pools].map(([k, v]) => [k, { at: v.at, companies: v.data.length }])), source: "东方财富 F10:公司概况、利润表、现金流量表(年报)" },
|
|
865
|
+
measures: FACETS,
|
|
866
|
+
companies: rows.map((r) => {
|
|
867
|
+
if (!("pct" in r)) return { code: r.company.code, name: r.company.name, org: r.company.org, industry: r.company.industry, market: r.company.market ?? "A股", note: /^[A-Z]/.test(r.company.code) ? `在申报上市(申报代码 ${r.company.code}),还没有公开的年报` : `没有 ${year} 年报数据(可能是当年之后才上市)` };
|
|
868
|
+
return {
|
|
869
|
+
code: r.company.code,
|
|
870
|
+
name: r.company.name,
|
|
871
|
+
org: r.company.org,
|
|
872
|
+
industry: r.company.industry,
|
|
873
|
+
market: r.company.market ?? "A股",
|
|
874
|
+
peerGroup: peerGroup(r.company).label,
|
|
875
|
+
...r.company.market === "新三板" ? { yardstick: "新三板公司,按同一证监会行业的 A 股上市公司来量" } : {},
|
|
876
|
+
.../^[A-Z]/.test(r.company.code) ? { yardstick: `在申报上市(申报代码 ${r.company.code}),财务数据来自申报材料,按同行业 A 股上市公司来量` } : {},
|
|
877
|
+
...r.company.peerSw ? { yardstick: `${r.company.market}公司,按申万一级「${r.company.peerSw}」的 A 股上市公司来量;不披露支付给职工的现金,待遇信号缺;金额是公司财报自己的币种,比较的都是比例,不受币种影响${r.company.approx ? `;「${r.company.name}」是按名字相近认的,请核对是不是你要的那家` : ""}` } : {},
|
|
878
|
+
years: r.years,
|
|
879
|
+
value: r.value,
|
|
880
|
+
percentile: r.pct,
|
|
881
|
+
peers: r.peers,
|
|
882
|
+
coverage: `${r.coverage}/4`,
|
|
883
|
+
more: r.more,
|
|
884
|
+
movedTogether: r.moved,
|
|
885
|
+
labels: r.labels,
|
|
886
|
+
outdoneBy: outdone.has(r.company.code) ? outdone.get(r.company.code).map((c) => ok.find((x) => x.company.code === c).company.name) : null,
|
|
887
|
+
under: Object.fromEntries(Object.entries(under).map(([k, v]) => [k, v.places.get(r.company.code)])),
|
|
888
|
+
anyWeighting: any.places.get(r.company.code)?.top3Share ?? null
|
|
889
|
+
};
|
|
890
|
+
}),
|
|
891
|
+
scenarios: Object.fromEntries(Object.entries(under).map(([k, v]) => [k, { weights: v.weights, samples: v.samples }])),
|
|
892
|
+
spread,
|
|
893
|
+
notFound,
|
|
894
|
+
method: `每项先在本行业同行里算百分位(越高越好),名次只在这几家之间排。「通常第 a–b 名」是在所选权重上下各 ${Math.round(spread * 100)} 个百分点内随机抽权重(固定种子)时名次的 10%–90% 范围;「进前三的比例」是这些权重里排进前三的占比,是偏好情景的比例,不是现实概率。缺数据的不估值,只给按已知指标的名次和最好 / 最坏情况。`
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
__name(compare, "compare");
|
|
898
|
+
var yi = /* @__PURE__ */ __name((v) => v === null || v === void 0 ? "—" : `${(v / 1e8).toFixed(2)} 亿`, "yi");
|
|
899
|
+
var pc = /* @__PURE__ */ __name((v) => v === null || v === void 0 ? "—" : `${(v * 100).toFixed(1)}%`, "pc");
|
|
900
|
+
function factsMarkdown(c, r) {
|
|
901
|
+
if (!c.years) return `## 一、事实(职路取数)
|
|
902
|
+
|
|
903
|
+
${c.note ?? "不是 A 股上市公司,职路没有它的同行数据。"}
|
|
904
|
+
`;
|
|
905
|
+
const ys = Object.keys(c.years).sort().reverse().slice(0, 4);
|
|
906
|
+
const row2 = /* @__PURE__ */ __name((label2, f) => `| ${label2} | ${ys.map((y) => f(c.years[y])).join(" | ")} |`, "row");
|
|
907
|
+
const pool = r.data.pools[c.peerGroup];
|
|
908
|
+
return [
|
|
909
|
+
`## 一、事实(职路取数,${r.year} 年报口径)`,
|
|
910
|
+
"",
|
|
911
|
+
`${c.name}(${c.code})· ${c.industry} · 员工 ${c.more.employees ?? "—"} 人(最新)`,
|
|
912
|
+
"",
|
|
913
|
+
`| 年份 | ${ys.join(" | ")} |`,
|
|
914
|
+
`|---|${ys.map(() => "---").join("|")}|`,
|
|
915
|
+
row2("营业总收入", (y) => yi(y.revenue)),
|
|
916
|
+
row2("归母净利润", (y) => yi(y.netProfit)),
|
|
917
|
+
row2("研发费用", (y) => yi(y.rd)),
|
|
918
|
+
row2("支付给职工的现金", (y) => yi(y.staffCash)),
|
|
919
|
+
row2("分配股利、利润或偿付利息的现金", (y) => yi(y.dividendCash)),
|
|
920
|
+
row2("购建长期资产的现金", (y) => yi(y.capex)),
|
|
921
|
+
"",
|
|
922
|
+
`在${c.peerGroup} ${pool?.companies ?? "?"} 家 A 股公司里的位置(百分位越高越好)${c.yardstick ? `;${c.yardstick}` : ""}:`,
|
|
923
|
+
"",
|
|
924
|
+
...FACETS.map((f) => `- ${f.label}(${f.measure}):${f.key === "pay" ? c.value.pay === null ? "—" : `${(c.value.pay / 1e4).toFixed(1)} 万/人` : pc(c.value[f.key])},同行第 ${c.percentile[f.key] ?? "—"} 百分位${f.note ? `(${f.note})` : ""}`),
|
|
925
|
+
`- 毛利率 ${pc(c.more.grossMargin)};购建长期资产占营收 ${pc(c.more.capexToRevenue)};分配股利和偿付利息的现金占净利 ${pc(c.more.dividendCashToProfit)}(含利息,不全是分红)`,
|
|
926
|
+
...c.movedTogether.length ? [`- ${r.year} 年同期一起发生的:${c.movedTogether.join(";")}`] : [],
|
|
927
|
+
...c.labels.length ? [`- 规则标签:${c.labels.map((l) => `${l.label}(${l.rule})`).join(";")}`] : [],
|
|
928
|
+
"",
|
|
929
|
+
`来源:${r.data.source},取数 ${pool?.at?.slice(0, 10) ?? "?"}。`,
|
|
930
|
+
""
|
|
931
|
+
].join("\n");
|
|
932
|
+
}
|
|
933
|
+
__name(factsMarkdown, "factsMarkdown");
|
|
934
|
+
|
|
935
|
+
// apps/daemon/src/service/fit.ts
|
|
936
|
+
import { parseJsonWithRepair, contentText } from "@earendil-works/pi-ai";
|
|
937
|
+
var SYSTEM = `你是校招岗位匹配评估员。给你一位应届生的画像和一个岗位,逐条对照岗位的硬性要求(学历、专业、技能、经验、证书、地点)和工作内容,判断他能不能投、合不合适。
|
|
938
|
+
只依据给出的内容,不要编造画像里没有的经历;岗位没写的要求不算缺口。
|
|
939
|
+
只输出 JSON:{"score": 0-100 的整数, "verdict": "一句话结论(20 字内)", "meets": ["满足的要求,每条带画像里的依据"], "gaps": ["不满足或存疑的要求,每条写明差在哪"]}
|
|
940
|
+
打分:硬性要求有明确不满足的不超过 40;硬性要求都满足且工作内容与其方向/研究/项目直接相关 80 以上;满足但方向只部分相关 55–79。`;
|
|
941
|
+
function profileText(s) {
|
|
942
|
+
return listFacts(s.domain.db).items.filter((f) => f.kind !== "exclude").map((f) => `- ${f.kind}:${f.statement || Object.values(f.data ?? {}).join(" / ")}`).join("\n");
|
|
943
|
+
}
|
|
944
|
+
__name(profileText, "profileText");
|
|
945
|
+
function postText(s, positionId) {
|
|
946
|
+
const p = s.domain.db.prepare(`select p.title, p.jd_text, p.degree_req, p.major_req, p.cities, c.degree_req c_degree, c.major_req c_major, c.cities c_cities, c.source_kind, co.name, co.type
|
|
947
|
+
from position p join campaign c on c.id = p.campaign_id join company co on co.id = c.company_id where p.id = ?`).get(positionId);
|
|
948
|
+
if (!p) return null;
|
|
949
|
+
const agg = isAggregate(p.source_kind);
|
|
950
|
+
return `公司:${p.name}(${p.type || "类型未知"})
|
|
951
|
+
岗位:${p.title}
|
|
952
|
+
地点:${json.parse(p.cities, []).join("、") || json.parse(p.c_cities, []).join("、") || "未写"}
|
|
953
|
+
学历要求:${p.degree_req || p.c_degree || "未写"}
|
|
954
|
+
专业要求:${p.major_req || (agg ? p.c_major : "") || "未写"}
|
|
955
|
+
JD:
|
|
956
|
+
${(p.jd_text || "(来源没有提供 JD 正文,只能按岗位名判断)").slice(0, 5e3)}`;
|
|
957
|
+
}
|
|
958
|
+
__name(postText, "postText");
|
|
959
|
+
async function deepFit(s, positionId) {
|
|
960
|
+
if (!s.llm) throw new Error("没有配置模型");
|
|
961
|
+
const post = postText(s, positionId);
|
|
962
|
+
if (!post) return null;
|
|
963
|
+
const profile = profileText(s);
|
|
964
|
+
const msg = await withPurpose("fit.deep", positionId, () => s.llm.models.completeSimple(s.llm.model(), { systemPrompt: SYSTEM, messages: [{ role: "user", content: [{ type: "text", text: `## 画像
|
|
965
|
+
${profile}
|
|
966
|
+
|
|
967
|
+
## 岗位
|
|
968
|
+
${post}` }], timestamp: Date.now() }] }, { reasoning: "low" }));
|
|
969
|
+
const o = parseJsonWithRepair(contentText(msg.content).trim().replace(/^```(?:json)?\s*|\s*```$/g, ""));
|
|
970
|
+
const list2 = /* @__PURE__ */ __name((v) => Array.isArray(v) ? v.map(String).slice(0, 10) : [], "list");
|
|
971
|
+
const score2 = Math.max(0, Math.min(100, Math.round(Number(o.score) || 0)));
|
|
972
|
+
s.domain.db.prepare(`insert into position_fit(position_id, score, verdict, meets, gaps, model, profile_hash, created_at) values (?,?,?,?,?,?,?,?)
|
|
973
|
+
on conflict(position_id) do update set score = excluded.score, verdict = excluded.verdict, meets = excluded.meets, gaps = excluded.gaps, model = excluded.model, profile_hash = excluded.profile_hash, created_at = excluded.created_at`).run(positionId, score2, String(o.verdict ?? "").slice(0, 60), json.str(list2(o.meets)), json.str(list2(o.gaps)), String(s.llm.model()?.id ?? ""), sha(profile), now());
|
|
974
|
+
return score2;
|
|
975
|
+
}
|
|
976
|
+
__name(deepFit, "deepFit");
|
|
977
|
+
var queue = [];
|
|
978
|
+
var failed = /* @__PURE__ */ new Map();
|
|
979
|
+
var running = false;
|
|
980
|
+
function enqueueFit(s, ids, opts = {}) {
|
|
981
|
+
const hash2 = sha(profileText(s));
|
|
982
|
+
const fresh2 = new Set(s.domain.db.prepare(`select position_id from position_fit where profile_hash = ?`).all(hash2).map((r) => r.position_id));
|
|
983
|
+
const todo = ids.filter((id) => (opts.force || !fresh2.has(id)) && !queue.includes(id));
|
|
984
|
+
queue.push(...todo);
|
|
985
|
+
for (const id of todo) failed.delete(id);
|
|
986
|
+
void drain2(s);
|
|
987
|
+
return { queued: todo.length, alreadyDone: opts.force ? 0 : ids.filter((id) => fresh2.has(id)).length, pending: queue.length };
|
|
988
|
+
}
|
|
989
|
+
__name(enqueueFit, "enqueueFit");
|
|
990
|
+
async function drain2(s) {
|
|
991
|
+
if (running) return;
|
|
992
|
+
running = true;
|
|
993
|
+
try {
|
|
994
|
+
let waits = 0;
|
|
995
|
+
while (queue.length) {
|
|
996
|
+
const id = queue[0];
|
|
997
|
+
try {
|
|
998
|
+
await deepFit(s, id);
|
|
999
|
+
queue.shift();
|
|
1000
|
+
s.onDataChanged?.();
|
|
1001
|
+
} catch (e) {
|
|
1002
|
+
if (e?.code === "MODEL_DELEGATED" && waits < 60) {
|
|
1003
|
+
waits++;
|
|
1004
|
+
await new Promise((r) => setTimeout(r, 6e4));
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
failed.set(id, String(e?.message ?? e).slice(0, 200));
|
|
1008
|
+
queue.shift();
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
} finally {
|
|
1012
|
+
running = false;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
__name(drain2, "drain");
|
|
1016
|
+
function listFit(s, q2) {
|
|
1017
|
+
const hash2 = sha(profileText(s));
|
|
1018
|
+
const rows = q2.companyId ? s.domain.db.prepare(`select f.* from position_fit f join position p on p.id = f.position_id join campaign c on c.id = p.campaign_id where c.company_id = ?`).all(q2.companyId) : s.domain.db.prepare(`select * from position_fit where position_id in (${(q2.positionIds ?? []).map(() => "?").join(",") || "''"})`).all(...q2.positionIds ?? []);
|
|
1019
|
+
return {
|
|
1020
|
+
items: rows.map((r) => ({ positionId: r.position_id, score: r.score, verdict: r.verdict, meets: json.parse(r.meets, []), gaps: json.parse(r.gaps, []), model: r.model, at: r.created_at, stale: r.profile_hash !== hash2 })),
|
|
1021
|
+
pending: [...queue],
|
|
1022
|
+
failed: Object.fromEntries(failed)
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
__name(listFit, "listFit");
|
|
1026
|
+
|
|
1027
|
+
// apps/daemon/src/agent/research.ts
|
|
512
1028
|
var RESEARCH_SCHEMA = {
|
|
513
1029
|
type: "object",
|
|
514
1030
|
additionalProperties: false,
|
|
@@ -576,6 +1092,136 @@ ${GRADE_RULES}
|
|
|
576
1092
|
最终只输出符合 schema 的 JSON(report_markdown 用中文,按上面 6 项分节,含"证据清单"一节把 evidence 再列一遍)。`;
|
|
577
1093
|
}
|
|
578
1094
|
__name(intelPrompt, "intelPrompt");
|
|
1095
|
+
var EMPLOYER_SCHEMA = {
|
|
1096
|
+
type: "object",
|
|
1097
|
+
additionalProperties: false,
|
|
1098
|
+
required: ["summary", "report_markdown", "evidence", "claims", "staff", "layoffs"],
|
|
1099
|
+
properties: {
|
|
1100
|
+
summary: { type: "string", description: "3 句以内:这家公司作为雇主的要点,不打分" },
|
|
1101
|
+
report_markdown: { type: "string", description: "中文 Markdown,从「## 二、人员:规模、人均薪资、裁员」写起:二、人员 / 三、利润为什么变 / 四、钱去了哪 / 五、激励和技术人员 / 六、上市与资本 / 七、求职信号 / 八、和你的匹配 / 九、证据清单" },
|
|
1102
|
+
staff: {
|
|
1103
|
+
type: "array",
|
|
1104
|
+
description: "近几年的年末员工数,每年一条,查不到的年份不写",
|
|
1105
|
+
items: { type: "object", additionalProperties: false, required: ["year", "employees", "source", "url"], properties: { year: { type: "number" }, employees: { type: "number" }, source: { type: "string", description: "年报「员工情况」/ 公示年报社保参保人数 / 招股书 / 其他(写明)" }, url: { type: "string" } } }
|
|
1106
|
+
},
|
|
1107
|
+
layoffs: {
|
|
1108
|
+
type: "array",
|
|
1109
|
+
description: "近三年裁员、降薪、欠薪、大规模离职的报道;没搜到就给空数组",
|
|
1110
|
+
items: { type: "object", additionalProperties: false, required: ["date", "what", "url"], properties: { date: { type: "string" }, what: { type: "string", description: "发生了什么、涉及哪些部门或多少人,照报道写" }, url: { type: "string" } } }
|
|
1111
|
+
},
|
|
1112
|
+
evidence: RESEARCH_SCHEMA.properties.evidence,
|
|
1113
|
+
claims: {
|
|
1114
|
+
type: "array",
|
|
1115
|
+
description: "报告里每条定性结论",
|
|
1116
|
+
items: {
|
|
1117
|
+
type: "object",
|
|
1118
|
+
additionalProperties: false,
|
|
1119
|
+
required: ["text", "type", "urls", "counter", "limits"],
|
|
1120
|
+
properties: { text: { type: "string" }, type: { type: "string", enum: ["事实", "解读", "推断"] }, urls: { type: "array", items: { type: "string" } }, counter: { type: "string", description: "找过的反面说法;没找到就写「没找到」" }, limits: { type: "string", description: "这条结论的限制" } }
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
};
|
|
1125
|
+
async function employerFacts(s, companyName) {
|
|
1126
|
+
try {
|
|
1127
|
+
const r = await compare(s.domain.db, { companies: [companyName] });
|
|
1128
|
+
const c = r.companies[0];
|
|
1129
|
+
return { md: c ? factsMarkdown(c, r) : factsMarkdown({ note: r.notFound[0]?.reason ?? "不是 A 股上市公司" }, r), staffCash: Object.fromEntries(Object.entries(c?.years ?? {}).map(([y, v]) => [y, v.staffCash])) };
|
|
1130
|
+
} catch (e) {
|
|
1131
|
+
return { staffCash: {}, md: `## 一、事实(职路取数)
|
|
1132
|
+
|
|
1133
|
+
这次没取到行业数据(${String(e?.message ?? e).slice(0, 120)}),下面的数字都来自调研员给的来源。
|
|
1134
|
+
` };
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
__name(employerFacts, "employerFacts");
|
|
1138
|
+
function employerPrompt(s, companyName, facts) {
|
|
1139
|
+
const db2 = s.domain.db;
|
|
1140
|
+
const co = db2.prepare("select id from company where name = ?").get(companyName);
|
|
1141
|
+
const intel2 = co ? db2.prepare("select kind, data_json from company_intel where company_id = ? and kind in ('announcements','dividends','ipo','listing','reports')").all(co.id) : [];
|
|
1142
|
+
const card2 = intel2.map((r) => {
|
|
1143
|
+
const d = JSON.parse(r.data_json);
|
|
1144
|
+
if (r.kind === "listing") return d.none ? "上市:巨潮、股转、美股都没查到" : `上市:${d.market} ${d.code}`;
|
|
1145
|
+
if (r.kind === "ipo") return `深交所项目库:${d.szse ? `${d.szse.stage}(${d.szse.date ?? ""})` : "未检出"}`;
|
|
1146
|
+
if (r.kind === "dividends") return `已实施的分红:${(d.items ?? []).map((x) => `${x.period} ${x.plan}`).join(";") || "无"}`;
|
|
1147
|
+
if (r.kind === "announcements") return `近一年相关公告:
|
|
1148
|
+
${(d.hits ?? []).filter((h) => /激励|持股|回购|减持|增持|上市|辅导|募集/.test(h.title)).slice(0, 20).map((h) => ` - ${h.date} ${h.title} ${h.url}`).join("\n") || " (无)"}`;
|
|
1149
|
+
if (r.kind === "reports") return `近一年研报标题:${(d.items ?? []).slice(0, 5).map((x) => x.title).join(";")}`;
|
|
1150
|
+
return "";
|
|
1151
|
+
}).filter(Boolean).join("\n");
|
|
1152
|
+
const posts = co ? db2.prepare("select p.title from position p join campaign c on c.id = p.campaign_id where c.company_id = ? and p.status = 'open' limit 40").all(co.id).map((p) => p.title) : [];
|
|
1153
|
+
const profile = profileText(s).split("\n").filter((l) => !l.startsWith("- basic")).join("\n");
|
|
1154
|
+
return `你是就业调研员,替一位求职者评估「${companyName}」这家雇主。今天:${now().slice(0, 10)}。
|
|
1155
|
+
|
|
1156
|
+
## 职路已经算好的事实(报告第一节由职路放,你不用重写;引用时数字照抄,别的数字要给来源)
|
|
1157
|
+
${facts}
|
|
1158
|
+
## 企业情况卡里已有的
|
|
1159
|
+
${card2 || "(还没有)"}
|
|
1160
|
+
|
|
1161
|
+
## 这家公司在岗位库里在招的
|
|
1162
|
+
${posts.length ? posts.join("、") : "(库里没有)"}
|
|
1163
|
+
|
|
1164
|
+
## 求职者资料
|
|
1165
|
+
${profile || "(资料是空的)"}
|
|
1166
|
+
|
|
1167
|
+
## 要回答的(证据不够就写「依据不足」,不要编)
|
|
1168
|
+
二、人员:规模、人均薪资、裁员。想知道的是:在不在裁员、人均拿多少、规模多大。
|
|
1169
|
+
- 近几年的年末员工数放进 staff,每年一条带来源:上市公司读年报「员工情况」的「在职员工的数量合计」;未上市的查国家企业信用信息公示系统年报里的「社保参保人数」(企查查、天眼查页面常有转载),或招股书。职路会用它和「支付给职工的现金」算每年人均,写在报告里,你不用算。
|
|
1170
|
+
- 搜近三年的裁员、降薪、欠薪、大规模离职报道(脉脉、新闻、黑猫投诉都可以),放进 layoffs,照报道写日期、发生了什么、涉及多少人和链接;没搜到就给空数组,在正文里写「没有搜到」,不等于没有。
|
|
1171
|
+
- 正文里写你对人员变化的解读:人数和工资总额怎么变的、和业务变化对得上吗。
|
|
1172
|
+
三、利润为什么变:营收、成本、人数分开说。只有年报「管理层讨论与分析」或公告写明的才说是原因,否则写「同时发生」并标待核实。是不是靠减员、降薪省出来的:和第二节的人数、职工现金对照。
|
|
1173
|
+
四、钱去了哪:研发、分红和回购、资本开支、并购,按上面的事实表和公告说。
|
|
1174
|
+
五、激励和技术人员:近三年的股权激励、员工持股计划,读公告原文,摘出激励对象(核心技术人员几人、占员工比例)、授予价格、考核条件;写明有没有覆盖技术人员。研发人员占比、技术岗在公司里的分量。
|
|
1175
|
+
六、上市与资本:已上市的写板块和近期再融资、减持;未上市的写融资轮次(天使、A、B…、战略投资)、每轮的时间、金额和主要投资方(来源:公司公告、36氪等媒体、企查查 / 天眼查公开页面,注明是哪一种),并查深交所、上交所、北交所的 IPO 项目库和证监会辅导备案,写「截至今天,以哪些名称在哪些库里没检出」,或者给出所处阶段和日期。
|
|
1176
|
+
七、求职信号:在招什么方向、口碑与风险(裁员、欠薪、诉讼、减持等新闻,注明日期和来源)。
|
|
1177
|
+
八、和这位求职者的匹配:优势、风险、未知各几条,单列,不并入对公司本身的判断。
|
|
1178
|
+
|
|
1179
|
+
规则:
|
|
1180
|
+
- 每条定性结论放进 claims:类型(事实 / 解读 / 推断)、证据 URL、找过的反面说法(没找到写「没找到」)、限制。
|
|
1181
|
+
- 不打分、不给总分、不写「推荐指数」,不替求职者决定去不去。
|
|
1182
|
+
${GRADE_RULES}
|
|
1183
|
+
最终只输出符合 schema 的 JSON(report_markdown 从「## 二、人员:规模、人均薪资、裁员」写起,最后一节「九、证据清单」把 evidence 再列一遍)。`;
|
|
1184
|
+
}
|
|
1185
|
+
__name(employerPrompt, "employerPrompt");
|
|
1186
|
+
async function startEmployerReport(s, companyName) {
|
|
1187
|
+
const company = s.domain.db.prepare("select id from company where name = ?").get(companyName);
|
|
1188
|
+
const companyId = company?.id ?? s.domain.upsertCompany(companyName);
|
|
1189
|
+
const facts = await employerFacts(s, companyName);
|
|
1190
|
+
const { runId } = spawnRun(s, { runner: "claude", mode: "research.company", subjectType: "company", subjectId: companyId, subjectLabel: companyName, prompt: employerPrompt(s, companyName, facts.md), schema: EMPLOYER_SCHEMA, maxTurns: 50 });
|
|
1191
|
+
const workflowId = s.workflows.start("research_run", runId, `research_run:${runId}`, { companyName, companyId, prefix: facts.md, staffCash: facts.staffCash });
|
|
1192
|
+
return { runId, workflowId };
|
|
1193
|
+
}
|
|
1194
|
+
__name(startEmployerReport, "startEmployerReport");
|
|
1195
|
+
function staffMarkdown(so, staffCash = {}) {
|
|
1196
|
+
const staff = (so.staff ?? []).filter((x) => x.employees > 0).sort((a, b) => b.year - a.year);
|
|
1197
|
+
const layoffs = so.layoffs ?? [];
|
|
1198
|
+
if (!so.staff && !so.layoffs) return "";
|
|
1199
|
+
const per = /* @__PURE__ */ __name((x) => {
|
|
1200
|
+
const c = staffCash[String(x.year)];
|
|
1201
|
+
return c ? c / x.employees : null;
|
|
1202
|
+
}, "per");
|
|
1203
|
+
const wan = /* @__PURE__ */ __name((v) => v === null ? "—" : `${(v / 1e4).toFixed(1)} 万`, "wan");
|
|
1204
|
+
const pctOf = /* @__PURE__ */ __name((a, b) => `${a >= b ? "+" : ""}${((a - b) / b * 100).toFixed(0)}%`, "pctOf");
|
|
1205
|
+
const out = ["### 人员(员工数由调研员查、带来源;职工现金和人均由职路算)", ""];
|
|
1206
|
+
if (staff.length) {
|
|
1207
|
+
out.push("| 年份 | 年末员工数 | 来源 | 支付给职工的现金 | 人均 |", "|---|---|---|---|---|");
|
|
1208
|
+
for (const x of staff) out.push(`| ${x.year} | ${x.employees} | [${x.source}](${x.url}) | ${staffCash[String(x.year)] ? `${(staffCash[String(x.year)] / 1e8).toFixed(2)} 亿` : "—"} | ${wan(per(x))} |`);
|
|
1209
|
+
const [last, first] = [staff[0], staff[staff.length - 1]];
|
|
1210
|
+
if (staff.length > 1) {
|
|
1211
|
+
const pl = per(last), pf = per(first);
|
|
1212
|
+
out.push("", `${first.year}→${last.year}:人数 ${first.employees}→${last.employees}(${pctOf(last.employees, first.employees)})${pl && pf ? `;人均 ${wan(pf)}→${wan(pl)}(${pctOf(pl, pf)})` : ""}。人均按年末人数算,含奖金、社保和离职补偿,裁员当年会偏高。`);
|
|
1213
|
+
}
|
|
1214
|
+
} else out.push("调研员没有查到历年员工数。");
|
|
1215
|
+
out.push("", layoffs.length ? "裁员、降薪等报道:" : "没有搜到近三年的裁员、降薪报道(没搜到不等于没有)。", ...layoffs.map((l) => `- ${l.date} ${l.what}([来源](${l.url}))`), "");
|
|
1216
|
+
return out.join("\n");
|
|
1217
|
+
}
|
|
1218
|
+
__name(staffMarkdown, "staffMarkdown");
|
|
1219
|
+
function claimsMarkdown(so) {
|
|
1220
|
+
const cell3 = /* @__PURE__ */ __name((x) => String(x ?? "").replace(/\|/g, "/").replace(/\s*\n\s*/g, " "), "cell");
|
|
1221
|
+
const claims = so.claims ?? [];
|
|
1222
|
+
return claims.length ? ["", "", "## 结论核对", "", "| 结论 | 类型 | 证据 | 反面说法 | 限制 |", "|---|---|---|---|---|", ...claims.map((c) => `| ${cell3(c.text)} | ${cell3(c.type)} | ${(c.urls ?? []).map((u, i) => `[${i + 1}](${u})`).join(" ") || "无"} | ${cell3(c.counter)} | ${cell3(c.limits)} |`)].join("\n") : "";
|
|
1223
|
+
}
|
|
1224
|
+
__name(claimsMarkdown, "claimsMarkdown");
|
|
579
1225
|
function startResearch(s, companyName, question = "") {
|
|
580
1226
|
const company = s.domain.db.prepare("select id from company where name = ?").get(companyName);
|
|
581
1227
|
const companyId = company?.id ?? s.domain.upsertCompany(companyName);
|
|
@@ -623,7 +1269,9 @@ summary: ${JSON.stringify(so.summary ?? "")}
|
|
|
623
1269
|
---
|
|
624
1270
|
|
|
625
1271
|
`;
|
|
626
|
-
writeFileSync3(path, header +
|
|
1272
|
+
writeFileSync3(path, header + (checkpoint.prefix ? `${checkpoint.prefix}
|
|
1273
|
+
${staffMarkdown(so, checkpoint.staffCash)}
|
|
1274
|
+
` : "") + so.report_markdown + claimsMarkdown(so) + "\n", "utf8");
|
|
627
1275
|
const ins = s.domain.db.prepare("insert into evidence(id, subject_type, subject_id, grade, url, title, quote, fetched_at, report_path, run_id) values (?,?,?,?,?,?,?,?,?,?)");
|
|
628
1276
|
for (const e of so.evidence ?? []) ins.run(ulid(), "company", checkpoint.companyId, e.grade ?? "推断", e.url ?? "", e.title ?? "", e.quote ?? "", now(), path, runId);
|
|
629
1277
|
s.domain.db.prepare("update agent_run set status = 'done', output_path = ?, summary = ?, session_id = ?, cost_usd = ?, finished_at = ? where id = ?").run(path, String(so.summary ?? "").slice(0, 500), r.sessionId ?? "", r.costUsd ?? 0, now(), runId);
|
|
@@ -971,98 +1619,6 @@ import { join as join5 } from "node:path";
|
|
|
971
1619
|
import { ulid as ulid3 } from "ulid";
|
|
972
1620
|
import YAML from "yaml";
|
|
973
1621
|
|
|
974
|
-
// apps/daemon/src/service/fit.ts
|
|
975
|
-
import { parseJsonWithRepair, contentText } from "@earendil-works/pi-ai";
|
|
976
|
-
var SYSTEM = `你是校招岗位匹配评估员。给你一位应届生的画像和一个岗位,逐条对照岗位的硬性要求(学历、专业、技能、经验、证书、地点)和工作内容,判断他能不能投、合不合适。
|
|
977
|
-
只依据给出的内容,不要编造画像里没有的经历;岗位没写的要求不算缺口。
|
|
978
|
-
只输出 JSON:{"score": 0-100 的整数, "verdict": "一句话结论(20 字内)", "meets": ["满足的要求,每条带画像里的依据"], "gaps": ["不满足或存疑的要求,每条写明差在哪"]}
|
|
979
|
-
打分:硬性要求有明确不满足的不超过 40;硬性要求都满足且工作内容与其方向/研究/项目直接相关 80 以上;满足但方向只部分相关 55–79。`;
|
|
980
|
-
function profileText(s) {
|
|
981
|
-
return listFacts(s.domain.db).items.filter((f) => f.kind !== "exclude").map((f) => `- ${f.kind}:${f.statement || Object.values(f.data ?? {}).join(" / ")}`).join("\n");
|
|
982
|
-
}
|
|
983
|
-
__name(profileText, "profileText");
|
|
984
|
-
function postText(s, positionId) {
|
|
985
|
-
const p = s.domain.db.prepare(`select p.title, p.jd_text, p.degree_req, p.major_req, p.cities, c.degree_req c_degree, c.major_req c_major, c.cities c_cities, c.source_kind, co.name, co.type
|
|
986
|
-
from position p join campaign c on c.id = p.campaign_id join company co on co.id = c.company_id where p.id = ?`).get(positionId);
|
|
987
|
-
if (!p) return null;
|
|
988
|
-
const agg = isAggregate(p.source_kind);
|
|
989
|
-
return `公司:${p.name}(${p.type || "类型未知"})
|
|
990
|
-
岗位:${p.title}
|
|
991
|
-
地点:${json.parse(p.cities, []).join("、") || json.parse(p.c_cities, []).join("、") || "未写"}
|
|
992
|
-
学历要求:${p.degree_req || p.c_degree || "未写"}
|
|
993
|
-
专业要求:${p.major_req || (agg ? p.c_major : "") || "未写"}
|
|
994
|
-
JD:
|
|
995
|
-
${(p.jd_text || "(来源没有提供 JD 正文,只能按岗位名判断)").slice(0, 5e3)}`;
|
|
996
|
-
}
|
|
997
|
-
__name(postText, "postText");
|
|
998
|
-
async function deepFit(s, positionId) {
|
|
999
|
-
if (!s.llm) throw new Error("没有配置模型");
|
|
1000
|
-
const post = postText(s, positionId);
|
|
1001
|
-
if (!post) return null;
|
|
1002
|
-
const profile = profileText(s);
|
|
1003
|
-
const msg = await withPurpose("fit.deep", positionId, () => s.llm.models.completeSimple(s.llm.model(), { systemPrompt: SYSTEM, messages: [{ role: "user", content: [{ type: "text", text: `## 画像
|
|
1004
|
-
${profile}
|
|
1005
|
-
|
|
1006
|
-
## 岗位
|
|
1007
|
-
${post}` }], timestamp: Date.now() }] }, { reasoning: "low" }));
|
|
1008
|
-
const o = parseJsonWithRepair(contentText(msg.content).trim().replace(/^```(?:json)?\s*|\s*```$/g, ""));
|
|
1009
|
-
const list2 = /* @__PURE__ */ __name((v) => Array.isArray(v) ? v.map(String).slice(0, 10) : [], "list");
|
|
1010
|
-
const score = Math.max(0, Math.min(100, Math.round(Number(o.score) || 0)));
|
|
1011
|
-
s.domain.db.prepare(`insert into position_fit(position_id, score, verdict, meets, gaps, model, profile_hash, created_at) values (?,?,?,?,?,?,?,?)
|
|
1012
|
-
on conflict(position_id) do update set score = excluded.score, verdict = excluded.verdict, meets = excluded.meets, gaps = excluded.gaps, model = excluded.model, profile_hash = excluded.profile_hash, created_at = excluded.created_at`).run(positionId, score, String(o.verdict ?? "").slice(0, 60), json.str(list2(o.meets)), json.str(list2(o.gaps)), String(s.llm.model()?.id ?? ""), sha(profile), now());
|
|
1013
|
-
return score;
|
|
1014
|
-
}
|
|
1015
|
-
__name(deepFit, "deepFit");
|
|
1016
|
-
var queue = [];
|
|
1017
|
-
var failed = /* @__PURE__ */ new Map();
|
|
1018
|
-
var running = false;
|
|
1019
|
-
function enqueueFit(s, ids, opts = {}) {
|
|
1020
|
-
const hash2 = sha(profileText(s));
|
|
1021
|
-
const fresh2 = new Set(s.domain.db.prepare(`select position_id from position_fit where profile_hash = ?`).all(hash2).map((r) => r.position_id));
|
|
1022
|
-
const todo = ids.filter((id) => (opts.force || !fresh2.has(id)) && !queue.includes(id));
|
|
1023
|
-
queue.push(...todo);
|
|
1024
|
-
for (const id of todo) failed.delete(id);
|
|
1025
|
-
void drain2(s);
|
|
1026
|
-
return { queued: todo.length, alreadyDone: opts.force ? 0 : ids.filter((id) => fresh2.has(id)).length, pending: queue.length };
|
|
1027
|
-
}
|
|
1028
|
-
__name(enqueueFit, "enqueueFit");
|
|
1029
|
-
async function drain2(s) {
|
|
1030
|
-
if (running) return;
|
|
1031
|
-
running = true;
|
|
1032
|
-
try {
|
|
1033
|
-
let waits = 0;
|
|
1034
|
-
while (queue.length) {
|
|
1035
|
-
const id = queue[0];
|
|
1036
|
-
try {
|
|
1037
|
-
await deepFit(s, id);
|
|
1038
|
-
queue.shift();
|
|
1039
|
-
s.onDataChanged?.();
|
|
1040
|
-
} catch (e) {
|
|
1041
|
-
if (e?.code === "MODEL_DELEGATED" && waits < 60) {
|
|
1042
|
-
waits++;
|
|
1043
|
-
await new Promise((r) => setTimeout(r, 6e4));
|
|
1044
|
-
continue;
|
|
1045
|
-
}
|
|
1046
|
-
failed.set(id, String(e?.message ?? e).slice(0, 200));
|
|
1047
|
-
queue.shift();
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
} finally {
|
|
1051
|
-
running = false;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
__name(drain2, "drain");
|
|
1055
|
-
function listFit(s, q) {
|
|
1056
|
-
const hash2 = sha(profileText(s));
|
|
1057
|
-
const rows = q.companyId ? s.domain.db.prepare(`select f.* from position_fit f join position p on p.id = f.position_id join campaign c on c.id = p.campaign_id where c.company_id = ?`).all(q.companyId) : s.domain.db.prepare(`select * from position_fit where position_id in (${(q.positionIds ?? []).map(() => "?").join(",") || "''"})`).all(...q.positionIds ?? []);
|
|
1058
|
-
return {
|
|
1059
|
-
items: rows.map((r) => ({ positionId: r.position_id, score: r.score, verdict: r.verdict, meets: json.parse(r.meets, []), gaps: json.parse(r.gaps, []), model: r.model, at: r.created_at, stale: r.profile_hash !== hash2 })),
|
|
1060
|
-
pending: [...queue],
|
|
1061
|
-
failed: Object.fromEntries(failed)
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
__name(listFit, "listFit");
|
|
1065
|
-
|
|
1066
1622
|
// apps/daemon/src/service/same-company.ts
|
|
1067
1623
|
var GROUPS = [
|
|
1068
1624
|
[/^(中国船舶(重工|工业)?集团(有限公司)?|中国船舶|中船集团|中船)/, "中船"],
|
|
@@ -1665,9 +2221,9 @@ ${m.text}`, "utf8");
|
|
|
1665
2221
|
return { message: msgRow(db2.prepare("select * from message where id = ?").get(id)), added: true };
|
|
1666
2222
|
}
|
|
1667
2223
|
__name(ingestMessage, "ingestMessage");
|
|
1668
|
-
function listMessages(db2,
|
|
1669
|
-
const where =
|
|
1670
|
-
const rows = db2.prepare(`select * from message ${where} order by received_at desc limit ?`).all(...
|
|
2224
|
+
function listMessages(db2, q2 = {}) {
|
|
2225
|
+
const where = q2.category ? "where category = ?" : q2.includeOther ? "" : "where category != '其他'";
|
|
2226
|
+
const rows = db2.prepare(`select * from message ${where} order by received_at desc limit ?`).all(...q2.category ? [q2.category] : [], q2.limit ?? 100);
|
|
1671
2227
|
return { items: rows.map(msgRow) };
|
|
1672
2228
|
}
|
|
1673
2229
|
__name(listMessages, "listMessages");
|
|
@@ -1897,9 +2453,9 @@ function pickResume(db2, positionId) {
|
|
|
1897
2453
|
const inTitle = x.roleKeywords.filter((k) => title.includes(low2(k)));
|
|
1898
2454
|
const inJd = x.roleKeywords.filter((k) => !title.includes(low2(k)) && jd.includes(low2(k)));
|
|
1899
2455
|
const types = x.companyTypes.filter((t) => ctype.includes(low2(t)) || industry.includes(low2(t)));
|
|
1900
|
-
const
|
|
2456
|
+
const score2 = 3 * inTitle.length + Math.min(inJd.length, 3) + (types.length ? 2 : 0);
|
|
1901
2457
|
const reason = [inTitle.length && `岗位名含 ${inTitle.join("/")}`, inJd.length && `JD 提到 ${inJd.join("/")}`, types.length && `公司类型 ${types.join("/")}`].filter(Boolean).join(";");
|
|
1902
|
-
return { resume: x, score, reason };
|
|
2458
|
+
return { resume: x, score: score2, reason };
|
|
1903
2459
|
}).sort((a, b) => b.score - a.score || Number(b.resume.isDefault) - Number(a.resume.isDefault) || a.resume.createdAt.localeCompare(b.resume.createdAt));
|
|
1904
2460
|
const best = scored[0] ?? null;
|
|
1905
2461
|
return {
|
|
@@ -2239,7 +2795,7 @@ var profileMatch = {
|
|
|
2239
2795
|
const cities = terms(facts.filter((f) => f.kind === "preference" && f.data.field === "城市").map((f) => f.data.value));
|
|
2240
2796
|
const gaps = s.domain.db.prepare("select concept_id from user_gap where status = 'confirmed'").all().map((r) => r.concept_id);
|
|
2241
2797
|
const me = userSide(facts, myLevel, gaps);
|
|
2242
|
-
if (!me.evidence.length && !majors.length) throw new DomainError("PROFILE_EMPTY", "
|
|
2798
|
+
if (!me.evidence.length && !majors.length) throw new DomainError("PROFILE_EMPTY", "你的资料里还没有可用的方向、技能或项目,无法匹配");
|
|
2243
2799
|
const distinct = new Set(me.evidence.map((e) => e.conceptId).filter((id) => concept(id)?.distinct));
|
|
2244
2800
|
const majorText = /* @__PURE__ */ __name((c) => c.majorReq || (/专业(?:要求)?[::]\s*([^\n;;。]+)/.exec(c.jd)?.[1] ?? "") || (c.agg ? c.campaignMajor : ""), "majorText");
|
|
2245
2801
|
const hits = [];
|
|
@@ -2316,12 +2872,12 @@ function expandHits(s, done) {
|
|
|
2316
2872
|
const official = new Set(campaigns.filter((c) => live.has(c.id) && !isAggregate(c.source_kind)).map((c) => c.company_id));
|
|
2317
2873
|
const name = db2.prepare("select name from company where id = ?");
|
|
2318
2874
|
const twinHasJobs = /* @__PURE__ */ __name((id) => !!done && (units.get(unitKey(name.get(id).name)) ?? []).some((c) => c.id !== id && official.has(c.id)), "twinHasJobs");
|
|
2319
|
-
for (const [companyId,
|
|
2875
|
+
for (const [companyId, score2] of best) {
|
|
2320
2876
|
const rows = open.all(companyId);
|
|
2321
2877
|
if (!rows.length || rows.some((r) => !isAggregate(r.source_kind)) || twinHasJobs(companyId)) continue;
|
|
2322
2878
|
const e = entryOf(rows.flatMap((r) => [r.apply_url, r.notice_url]).filter(Boolean));
|
|
2323
2879
|
if (e.url && done?.has(siteKeyOf(e.url))) continue;
|
|
2324
|
-
hits.push({ companyId, score:
|
|
2880
|
+
hits.push({ companyId, score: score2 + (e.readable ? 1 : 0), reason: `只有汇总表信息;${e.label}` });
|
|
2325
2881
|
}
|
|
2326
2882
|
const groups = db2.prepare(`select id, industry from company where type = '央国企' and name not like '%(%' and name not like '%(%'
|
|
2327
2883
|
and (name like '%集团' or name like '%集团有限公司' or (name like '中国%' and length(name) <= 6))`).all();
|
|
@@ -2459,7 +3015,7 @@ function pruneAutoSources(s) {
|
|
|
2459
3015
|
const cared = /* @__PURE__ */ new Set();
|
|
2460
3016
|
for (let i = 0; i < ids.length; i += 500) for (const r of s.domain.db.prepare(`select name from company where id in (${ids.slice(i, i + 500).map(() => "?").join(",")})`).all(...ids.slice(i, i + 500))) cared.add(r.name);
|
|
2461
3017
|
const seen2 = /* @__PURE__ */ new Set();
|
|
2462
|
-
const
|
|
3018
|
+
const keep2 = s.config.sources.filter((c) => {
|
|
2463
3019
|
const key = c.url ? siteKeyOf(c.url) : c.id;
|
|
2464
3020
|
if (!c.auto || c.discover) {
|
|
2465
3021
|
seen2.add(key);
|
|
@@ -2470,8 +3026,8 @@ function pruneAutoSources(s) {
|
|
|
2470
3026
|
return true;
|
|
2471
3027
|
});
|
|
2472
3028
|
const before = s.config.sources.length;
|
|
2473
|
-
if (
|
|
2474
|
-
return { before, after:
|
|
3029
|
+
if (keep2.length !== before) saveSources(dataOf(s), keep2);
|
|
3030
|
+
return { before, after: keep2.length };
|
|
2475
3031
|
}
|
|
2476
3032
|
__name(pruneAutoSources, "pruneAutoSources");
|
|
2477
3033
|
|
|
@@ -4515,7 +5071,7 @@ async function browserRead(s, url, input, finish2) {
|
|
|
4515
5071
|
});
|
|
4516
5072
|
await opened.done();
|
|
4517
5073
|
}, "done");
|
|
4518
|
-
let
|
|
5074
|
+
let keep2 = false;
|
|
4519
5075
|
try {
|
|
4520
5076
|
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 2e4 }).catch(() => {
|
|
4521
5077
|
});
|
|
@@ -4531,14 +5087,14 @@ async function browserRead(s, url, input, finish2) {
|
|
|
4531
5087
|
const r = await extract(s, page, url, input, finish2, opened.page);
|
|
4532
5088
|
page = r.pageOut;
|
|
4533
5089
|
if (input.mode === "open" && r.result.status !== "blocked") {
|
|
4534
|
-
|
|
5090
|
+
keep2 = true;
|
|
4535
5091
|
const x = { id: ulid6(), page, done, origin, actions: (input.steps?.length ?? 0) + 1, createdAt: Date.now(), lastUsed: Date.now(), chain: Promise.resolve() };
|
|
4536
5092
|
sessions.set(x.id, x);
|
|
4537
5093
|
return { ...r.result, session: sessionInfo(x, "ready") };
|
|
4538
5094
|
}
|
|
4539
5095
|
return r.result;
|
|
4540
5096
|
} finally {
|
|
4541
|
-
if (!
|
|
5097
|
+
if (!keep2) await done();
|
|
4542
5098
|
}
|
|
4543
5099
|
}
|
|
4544
5100
|
__name(browserRead, "browserRead");
|
|
@@ -4854,14 +5410,14 @@ function pickCandidates(rows, applied) {
|
|
|
4854
5410
|
return out.sort((a, b) => b.prefScore - a.prefScore || String(a.closesAt ?? "9").localeCompare(String(b.closesAt ?? "9")));
|
|
4855
5411
|
}
|
|
4856
5412
|
__name(pickCandidates, "pickCandidates");
|
|
4857
|
-
function exploreCandidates(s,
|
|
5413
|
+
function exploreCandidates(s, q2 = {}) {
|
|
4858
5414
|
const db2 = s.domain.db;
|
|
4859
5415
|
const since = s.config.season?.since ?? new Date(Date.now() - 180 * 864e5).toISOString().slice(0, 10);
|
|
4860
5416
|
const applied = /* @__PURE__ */ new Map();
|
|
4861
5417
|
for (const r of db2.prepare("select c.company_id id, count(*) n from application a join position p on p.id = a.position_id join campaign c on c.id = p.campaign_id where a.state != 'cancelled' and a.created_at >= ? group by c.company_id").all(since)) applied.set(r.id, r.n);
|
|
4862
5418
|
const { items } = listPool(db2, { recommendedOnly: true, minTier: "可迁移", hideExpectationFail: true, limit: 5e3, sort: "match" });
|
|
4863
5419
|
const all = pickCandidates(items, applied);
|
|
4864
|
-
return { season: s.config.season?.key ?? "", since, total: all.length, items: all.slice(0,
|
|
5420
|
+
return { season: s.config.season?.key ?? "", since, total: all.length, items: all.slice(0, q2.limit ?? 50) };
|
|
4865
5421
|
}
|
|
4866
5422
|
__name(exploreCandidates, "exploreCandidates");
|
|
4867
5423
|
|
|
@@ -5332,7 +5888,7 @@ function login(s) {
|
|
|
5332
5888
|
return manager ??= createOAuthLoginManager({ models: s.llm.models });
|
|
5333
5889
|
}
|
|
5334
5890
|
__name(login, "login");
|
|
5335
|
-
var
|
|
5891
|
+
var remembered2 = /* @__PURE__ */ __name((s, provider) => (YAML2.parse(readFileSync13(configFile(s.config.vault), "utf8"))?.llm?.selections ?? {})[provider] ?? "", "remembered");
|
|
5336
5892
|
function persist(s, provider, model) {
|
|
5337
5893
|
updateConfig(s.config.vault, (doc) => {
|
|
5338
5894
|
doc.llm = { ...doc.llm ?? {}, provider, model, selections: { ...doc.llm?.selections ?? {}, ...model ? { [provider]: model } : {} } };
|
|
@@ -5406,7 +5962,7 @@ async function modelProvider(s, providerId, authType) {
|
|
|
5406
5962
|
if (phase === "running" || phase === "waiting_for_input") lm.cancel();
|
|
5407
5963
|
chosenAuth = authType;
|
|
5408
5964
|
if (providerId !== s.config.llm.provider) {
|
|
5409
|
-
const list2 = listProviderModels(s.llm.models, providerId), was =
|
|
5965
|
+
const list2 = listProviderModels(s.llm.models, providerId), was = remembered2(s, providerId);
|
|
5410
5966
|
persist(s, providerId, list2.some((m) => m.id === was) ? was : list2[0]?.id ?? "");
|
|
5411
5967
|
}
|
|
5412
5968
|
return modelStatus(s);
|
|
@@ -5471,13 +6027,13 @@ __name(tagCompany, "tagCompany");
|
|
|
5471
6027
|
import { join as join12 } from "node:path";
|
|
5472
6028
|
|
|
5473
6029
|
// apps/daemon/src/service/opportunity-gaps.ts
|
|
5474
|
-
function listGaps(s,
|
|
6030
|
+
function listGaps(s, q2 = {}) {
|
|
5475
6031
|
const all = s.domain.db.prepare(`select a.position_id, a.position_id id, p.campaign_id, p.closes_at, a.json, p.title, co.name company, c.company_id from position_assessment a
|
|
5476
6032
|
join position p on p.id = a.position_id join campaign c on c.id = p.campaign_id join company co on co.id = c.company_id
|
|
5477
6033
|
where p.status != 'closed'`).all();
|
|
5478
6034
|
const expired2 = expiredMany(s.domain.db, all);
|
|
5479
6035
|
const rows = all.filter((r) => !expired2.get(r.id));
|
|
5480
|
-
const minA =
|
|
6036
|
+
const minA = q2.minA ?? 30;
|
|
5481
6037
|
const acc = /* @__PURE__ */ new Map();
|
|
5482
6038
|
let related = 0;
|
|
5483
6039
|
for (const r of rows) {
|
|
@@ -5833,7 +6389,8 @@ var handlers = {
|
|
|
5833
6389
|
s.onDataChanged?.();
|
|
5834
6390
|
return r;
|
|
5835
6391
|
}, "import.csv"),
|
|
5836
|
-
"listing.capture": /* @__PURE__ */ __name(async (i,
|
|
6392
|
+
"listing.capture": /* @__PURE__ */ __name(async (i, a, s) => {
|
|
6393
|
+
if (a.kind === "agent" && providerOf(i.url).provider === "boss") throw new ToolError("USE_BOOKMARK", "BOSS 直聘的岗位请用户自己在浏览器里打开,用捕获书签收;BOSS 的用户协议不允许程序代为读取");
|
|
5837
6394
|
const pull2 = sourceFor(i.url);
|
|
5838
6395
|
if (pull2?.fetch) {
|
|
5839
6396
|
const tenantUrl = pull2.id === "moka" ? i.url.split("#")[0] : i.url;
|
|
@@ -5886,7 +6443,7 @@ var handlers = {
|
|
|
5886
6443
|
s.onDataChanged?.();
|
|
5887
6444
|
return { id: i.id, name: cfg.name ?? "", droppedCampaigns };
|
|
5888
6445
|
}, "source.remove"),
|
|
5889
|
-
"research.company": /* @__PURE__ */ __name((i, _a, s) => startResearch(s, i.companyName, i.question), "research.company"),
|
|
6446
|
+
"research.company": /* @__PURE__ */ __name((i, _a, s) => i.question === "就业评估" ? startEmployerReport(s, i.companyName) : startResearch(s, i.companyName, i.question), "research.company"),
|
|
5890
6447
|
"research.topic": /* @__PURE__ */ __name(async (i, _a, s) => {
|
|
5891
6448
|
try {
|
|
5892
6449
|
return await startTopicResearch(s, i);
|
|
@@ -5898,6 +6455,7 @@ var handlers = {
|
|
|
5898
6455
|
"industry.reports": /* @__PURE__ */ __name((i, _a, s) => dataTool("industry.reports", i, s), "industry.reports"),
|
|
5899
6456
|
"industry.companies": /* @__PURE__ */ __name((i, _a, s) => dataTool("industry.companies", i, s), "industry.companies"),
|
|
5900
6457
|
"company.financials": /* @__PURE__ */ __name((i, _a, s) => dataTool("company.financials", i, s), "company.financials"),
|
|
6458
|
+
"employer.compare": /* @__PURE__ */ __name((i, _a, s) => compare(s.domain.db, i), "employer.compare"),
|
|
5901
6459
|
"labor.us": /* @__PURE__ */ __name((i, _a, s) => dataTool("labor.us", i, s), "labor.us"),
|
|
5902
6460
|
"research.report": /* @__PURE__ */ __name((i, _a, s) => {
|
|
5903
6461
|
const r = getTopic(s, i.runId);
|
|
@@ -6080,9 +6638,9 @@ var handlers = {
|
|
|
6080
6638
|
"company.official": /* @__PURE__ */ __name((i, _a, s) => ({ links: officialLinks(s, i.companyId) }), "company.official"),
|
|
6081
6639
|
"company.syncOfficial": /* @__PURE__ */ __name((i, _a, s) => syncOfficial(s, i.companyId), "company.syncOfficial"),
|
|
6082
6640
|
"company.search": /* @__PURE__ */ __name((i, _a, s) => searchCompanies(s.domain.db, i.query, i.limit ?? 10), "company.search"),
|
|
6083
|
-
"company.intel": /* @__PURE__ */ __name((i, _a, s) =>
|
|
6641
|
+
"company.intel": /* @__PURE__ */ __name((i, _a, s) => readIntel(s, i.companyId), "company.intel"),
|
|
6084
6642
|
"company.intelDetail": /* @__PURE__ */ __name((i, _a, s) => getIntelDetail(s, i.companyId, i.kind), "company.intelDetail"),
|
|
6085
|
-
"company.intelRefresh": /* @__PURE__ */ __name((i, _a, s) => refreshIntel(s, i.companyId), "company.intelRefresh"),
|
|
6643
|
+
"company.intelRefresh": /* @__PURE__ */ __name((i, _a, s) => refreshIntel(s, i.companyId, { force: i.force !== false }), "company.intelRefresh"),
|
|
6086
6644
|
"intel.smeImport": /* @__PURE__ */ __name((i, _a, s) => importSme(s.domain.db, s.config.vault, i.path), "intel.smeImport"),
|
|
6087
6645
|
"channel.list": /* @__PURE__ */ __name((_i, _a, s) => ({ items: listChannels(s) }), "channel.list"),
|
|
6088
6646
|
"intel.smeFromUrl": /* @__PURE__ */ __name((i, _a, s) => loadSources(s.domain.db, [{ url: i.url, level: i.level, batch: i.batch, region: i.region, published_at: i.publishedAt }]), "intel.smeFromUrl"),
|
|
@@ -6114,6 +6672,13 @@ var handlers = {
|
|
|
6114
6672
|
setAgentConfig(s, { backend: i.backend });
|
|
6115
6673
|
return { backend: i.backend };
|
|
6116
6674
|
}, "agent.backend"),
|
|
6675
|
+
// a model for Zhiro's runs of Claude Code / Codex; "" = follow the harness's own setting. Their settings are not touched.
|
|
6676
|
+
"agent.model": /* @__PURE__ */ __name((i, a, s) => {
|
|
6677
|
+
humanOnly(a);
|
|
6678
|
+
setAgentConfig(s, { models: { ...s.config.agent?.models ?? {}, [i.backend]: i.model } });
|
|
6679
|
+
chooseModel(i.backend, i.model);
|
|
6680
|
+
return { backend: i.backend, model: i.model, using: followedModel(i.backend) };
|
|
6681
|
+
}, "agent.model"),
|
|
6117
6682
|
"agent.harnessConfirm": /* @__PURE__ */ __name((i, a, s) => {
|
|
6118
6683
|
humanOnly(a);
|
|
6119
6684
|
setAgentConfig(s, { harnessConfirm: i.where });
|
|
@@ -6168,7 +6733,7 @@ var handlers = {
|
|
|
6168
6733
|
llm: { ...s.config.llm, ...s.llm ? await s.llm.status() : {} },
|
|
6169
6734
|
worker: workerStatus(),
|
|
6170
6735
|
// ADR-046 / 047: who answers the drawer, what is installed; where the person's own harness confirms
|
|
6171
|
-
chat: { backend: backendOf(s), backends: s.chat?.status() ?? {}, harnessConfirm: harnessConfirm(s), agentDir: join12(s.config.vault, "agent"), models: { claude: followedModel("claude"), codex: followedModel("codex"), refused: { claude: modelRefused("claude"), codex: modelRefused("codex") } } },
|
|
6736
|
+
chat: { backend: backendOf(s), backends: s.chat?.status() ?? {}, harnessConfirm: harnessConfirm(s), agentDir: join12(s.config.vault, "agent"), models: { claude: followedModel("claude"), codex: followedModel("codex"), refused: { claude: modelRefused("claude"), codex: modelRefused("codex") }, own: { claude: harnessModel("claude"), codex: harnessModel("codex") }, chosen: s.config.agent?.models ?? {}, choices: { claude: harnessModels("claude"), codex: harnessModels("codex") } } },
|
|
6172
6737
|
background: { useModel: backgroundOnModel(s), doneBy: describeBackground(s) },
|
|
6173
6738
|
applyReference: s.config.apply?.referenceDoc ?? "",
|
|
6174
6739
|
configFile: s.config.vault + "\\config\\jobos.yaml"
|
|
@@ -6232,7 +6797,7 @@ function executeDirect(name, input, actor, s) {
|
|
|
6232
6797
|
return h(input, actor, s);
|
|
6233
6798
|
}
|
|
6234
6799
|
__name(executeDirect, "executeDirect");
|
|
6235
|
-
var HUMAN_ONLY = /* @__PURE__ */ new Set(["gap.confirm", "model.login", "model.answer", "model.cancel", "model.provider", "model.select", "model.refresh", "inbox.connect", "inbox.disconnect", "history.undo", "agent.mode", "agent.backend", "agent.harnessConfirm", "agent.background", "data.export", "data.move"]);
|
|
6800
|
+
var HUMAN_ONLY = /* @__PURE__ */ new Set(["agent.model", "gap.confirm", "model.login", "model.answer", "model.cancel", "model.provider", "model.select", "model.refresh", "inbox.connect", "inbox.disconnect", "history.undo", "agent.mode", "agent.backend", "agent.harnessConfirm", "agent.background", "data.export", "data.move"]);
|
|
6236
6801
|
var callableBy = /* @__PURE__ */ __name((actor, s) => {
|
|
6237
6802
|
const harness = !!s && mayConfirmInHarness(actor, s);
|
|
6238
6803
|
return listContracts().filter((c) => actor.kind === "human" || harness && (c.name === "proposal.decide" || c.name === "history.undo") || !HUMAN_ONLY.has(c.name) && c.name !== "proposal.decide");
|
|
@@ -6809,7 +7374,7 @@ ${dl}`, `待决提案:${today.pendingProposals};待办下一步:${today.ne
|
|
|
6809
7374
|
"岗位的 match.reason 以 待补资料 开头、或岗位名只是类别(「机械工程」「技术类」)、或 position_get 没有岗位描述时,不要据此判断合不合适:先 lead_resolve({companyIds:[…]}) 让系统读公告页补出具体岗位(几秒到一分钟),再用 pool_list 看补出来的岗位;仍补不出就如实说「这家只有类别,入口是 X,需要手动查看」。判断合不合适看 match.reason 里的档位与待核实项,细看用 position_get。",
|
|
6810
7375
|
"一条消息里有 3 题以上互不依赖的调研任务(各自要读不同网页):用 research_dispatch 一次派出全部题目(itemId、题目原文、该题的输出格式),等结果回来后按用户要求的格式汇总,status 不是 answered 的题照实写明原因;不要自己逐题读页。",
|
|
6811
7376
|
"长调研(要读很多页才能回答):每拿到一题就立刻 research_checkpoint 记下该题的最终内容和链接——较早的页面内容会被省略,没记的就没有了,收尾时没记录的题按未完成处理;先查岗位库(pool_list / company_search / position_get:8 千家公司的官方入口、国能等站的全量岗位都在库里),库里没有再 page_read;同一站点要连做几步(选单位 → 点岗位 → 下一页)用 page_read 的 mode=open + continue,不要每次从首页重来;读列表页后按 links 读详情;不同站点、不同题目的 page_read 在同一条消息里一起发起(它们会并行执行,一条一条发要慢好几倍);一个站点读不到(needsLogin / blocked)就标 blocked 换下一题,不要反复重试;全部题目记完调用 research_finish,再按用户要求的格式写出完整答案。",
|
|
6812
|
-
"问一家公司的经营状况(收益、研发、员工数、分红、专精特新、IPO、扩产、投资)先 company_intel
|
|
7377
|
+
"问一家公司的经营状况(收益、研发、员工数、分红、专精特新、IPO、扩产、投资)先 company_intel;过期的类会在后台自动更新(refreshing=true 时稍后再读);从没生成过(listing 为空)才调 company_intelRefresh;非上市公司只有名录匹配,其余用 research_company 派联网调研(会出确认卡)。回答时带上每类数据的更新日期。",
|
|
6813
7378
|
"问一个行业或方向(规模、哪些公司、谁在赚钱、薪资):直接用数据工具取数再答——industry_companies(这个主题在股市里的上市公司,候选名单)、company_financials(按公司名或代码查任何上市公司的营收 / 净利 / 研发 / 员工数,不必在岗位库里)、industry_reports(券商最近的研报)、labor_us(美国某职业的就业人数与年薪)。回答里写明数字的财年、币种和来源。要一份完整的报告再用 research_topic(会出确认卡)。",
|
|
6814
7379
|
"工具返回里如果有 changedByUser,是用户在 Zhiro 窗口里自你上次调用以来改过的东西(一行一处):以它为准更新你记着的情况,必要时重新读,不要把旧的说法再告诉用户。",
|
|
6815
7380
|
"批量删除、批量结束申请这类影响多条数据的操作,先说清会影响哪些、请用户确认,再调用工具。授权码、密码、模型登录只能由用户在界面里操作,不要在对话里索要或转交。",
|
|
@@ -7329,6 +7894,7 @@ try {
|
|
|
7329
7894
|
}
|
|
7330
7895
|
throw e;
|
|
7331
7896
|
}
|
|
7897
|
+
for (const n of ["claude", "codex"]) chooseModel(n, config.agent?.models?.[n] ?? "");
|
|
7332
7898
|
var lock;
|
|
7333
7899
|
try {
|
|
7334
7900
|
lock = await lockVault(config.vault, "daemon");
|