feat: 项目层级树+合作方绑定KOC资源+客户群全量可选+精简编辑表单,移除复盘报表

- 项目支持父子层级:树形折叠展示、详情面板内联管理总群成员、推送聚合子项目任务、环检测与子项目删除守卫
- 外部KOC合作方绑定 KOC 资源账号(唯一占用、换绑/解绑、资源页一键设为合作方),列表直显联系方式
- 绑定客户群改为全量外部群缓存(含各群主),同步按钮强刷;member_count 缺失以 member_list 兜底
- 任务/项目编辑模式隐藏不可变字段(平台/内容类型/品牌/总群成员),详情面板维护成员时保留 parent_id
- 删除复盘报表整栏(导航/页面/task_report_list/导出路由/CSV lib 及测试)
This commit is contained in:
ABAPPLO
2026-09-23 11:13:17 +08:00
parent 8f1ebf7697
commit ab78b187c0
20 changed files with 1354 additions and 958 deletions

View File

@@ -2,7 +2,6 @@ import assert from "node:assert/strict";
import test from "node:test";
import {
bindTaskCustomerChat,
listOwnerCustomerChats,
unbindTaskCustomerChat,
} from "../lib/task-customer-chat-service.ts";
import { resolveWecomConfig } from "../lib/wecom-client.ts";
@@ -91,14 +90,6 @@ function callsTo(calls, suffix) {
return calls.filter((c) => c.url.pathname.endsWith(suffix));
}
test("listOwnerCustomerChats rejects without app credentials and ignores empty owner", async () => {
const config = resolveWecomConfig({ ...baseBindings, WECOM_SECRET: "" });
await assert.rejects(() => listOwnerCustomerChats(config, "zhangsan"), /未配置/);
const { calls, fetchImpl } = fakeWecom();
assert.deepEqual(await listOwnerCustomerChats(resolveWecomConfig(baseBindings), " ", fetchImpl), []);
assert.equal(calls.length, 0, "empty owner short-circuits without API calls");
});
test("bindTaskCustomerChat creates join way when task has no config", async () => {
const { db, captured } = captureDb({
id: "task-1",