feat(asset-ai): add contextual analysis and unmet-demand feedback

This commit is contained in:
shishengliang
2026-09-23 16:01:10 +08:00
parent 0a32392215
commit 7e9760f5db
23 changed files with 5160 additions and 835 deletions
+79
View File
@@ -0,0 +1,79 @@
import { createHash } from "node:crypto";
import type { RowDataPacket } from "mysql2";
import pool from "../../db.js";
import type {
DemandGap,
GapReason,
GapStatus,
} from "../../../shared/assets/assistant.js";
// Store the unmet request and concise analysis context for product feedback, not full sessions or result rows.
let ready: Promise<void> | undefined;
const purge = () =>
pool.query(
"DELETE FROM bi_asset_ai_gaps WHERE last_seen < DATE_SUB(UTC_TIMESTAMP(), INTERVAL 90 DAY)",
);
const SCHEMA = `CREATE TABLE IF NOT EXISTS bi_asset_ai_gaps (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id VARCHAR(64) NOT NULL, fingerprint CHAR(64) NOT NULL,
demand VARCHAR(600) NOT NULL, reason VARCHAR(24) NOT NULL, status VARCHAR(24) NOT NULL DEFAULT 'open',
occurrences INT NOT NULL DEFAULT 1, first_seen DATETIME NOT NULL, last_seen DATETIME NOT NULL,
UNIQUE KEY user_demand (user_id, fingerprint), KEY last_seen (last_seen)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`;
async function ensure() {
if (!ready)
ready = (async () => {
// Precreated production tables need no DDL permission.
try {
await pool.query("SELECT id FROM bi_asset_ai_gaps LIMIT 0");
} catch (error) {
if ((error as { code?: string }).code !== "ER_NO_SUCH_TABLE")
throw error;
await pool.query(SCHEMA);
}
await purge();
setInterval(
() => {
void purge().catch(() =>
console.warn("asset_ai_gap_retention_failed"),
);
},
60 * 60 * 1000,
).unref();
})().catch((error) => {
ready = undefined;
throw error;
});
await ready;
}
export const gapStore = {
async record(userId: string, demand: string, reason: GapReason) {
await ensure();
const fingerprint = createHash("sha256")
.update(reason + "|" + demand)
.digest("hex");
await pool.query(
`INSERT INTO bi_asset_ai_gaps (user_id,fingerprint,demand,reason,first_seen,last_seen)
VALUES (?,?,?,?,UTC_TIMESTAMP(),UTC_TIMESTAMP()) ON DUPLICATE KEY UPDATE
occurrences=occurrences+1, last_seen=UTC_TIMESTAMP(), status=IF(status='resolved','open',status)`,
[userId, fingerprint, demand, reason],
);
},
async list(userId: string | null): Promise<DemandGap[]> {
await ensure();
await purge();
const [rows] = await pool.query<RowDataPacket[]>(
`SELECT id,demand,reason,status,occurrences,
DATE_FORMAT(first_seen,'%Y-%m-%dT%H:%i:%sZ') firstSeen, DATE_FORMAT(last_seen,'%Y-%m-%dT%H:%i:%sZ') lastSeen
FROM bi_asset_ai_gaps ${userId ? "WHERE user_id = ?" : ""} ORDER BY last_seen DESC LIMIT 200`,
userId ? [userId] : [],
);
return rows as DemandGap[];
},
async update(id: number, status: GapStatus) {
await ensure();
await pool.query("UPDATE bi_asset_ai_gaps SET status=? WHERE id=?", [
status,
id,
]);
},
};