feat(archival): move client archiving from mock to API and SQLite
This commit is contained in:
parent
d711df312f
commit
cd985e38ea
@ -1530,23 +1530,34 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
|
|||||||
if not user:
|
if not user:
|
||||||
return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)
|
return web.json_response({"error": "Unauthorized"}, status=401, headers=_CORS_HEADERS)
|
||||||
|
|
||||||
userDir = self._getUserStorageDir(user.get("sub", "unknown"))
|
try:
|
||||||
clientsDir = os.path.join(userDir, "clients")
|
from hermes_state import SessionDB
|
||||||
|
db = SessionDB()
|
||||||
clients = []
|
with db._lock:
|
||||||
if os.path.exists(clientsDir):
|
cursor = db._conn.execute(
|
||||||
for d in os.listdir(clientsDir):
|
"SELECT client_id, name, phone, created_at, updated_at FROM deepview_clients WHERE user_id=? ORDER BY updated_at DESC",
|
||||||
clientPath = os.path.join(clientsDir, d)
|
(user["userId"],)
|
||||||
if os.path.isdir(clientPath):
|
)
|
||||||
# 尝试读取 profile.md 以获取首行做名字(可选优化)
|
rows = cursor.fetchall()
|
||||||
# 默认使用目录名作为 ID 和名字
|
|
||||||
clients.append({
|
clients = []
|
||||||
"id": d,
|
for row in rows:
|
||||||
"name": d,
|
c_id, c_name, c_phone, c_created, c_updated = row
|
||||||
"hasProfile": os.path.exists(os.path.join(clientPath, "profile.md"))
|
|
||||||
})
|
# Option 1: To get report count, we could do SQL count or just skip it for now.
|
||||||
|
# Here we just map directly.
|
||||||
return web.json_response({"clients": clients}, headers=_CORS_HEADERS)
|
clients.append({
|
||||||
|
"id": c_id,
|
||||||
|
"name": c_name,
|
||||||
|
"phone": c_phone,
|
||||||
|
"createdAt": c_created,
|
||||||
|
"updatedAt": c_updated
|
||||||
|
})
|
||||||
|
|
||||||
|
return web.json_response({"clients": clients}, headers=_CORS_HEADERS)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[DeepviewSSE] DB Clients list failed: {e}")
|
||||||
|
return web.json_response({"error": str(e)}, status=500, headers=_CORS_HEADERS)
|
||||||
async def _handleClientCreate(self, request: "web.Request") -> "web.Response":
|
async def _handleClientCreate(self, request: "web.Request") -> "web.Response":
|
||||||
"""
|
"""
|
||||||
POST /deepview/clients/create
|
POST /deepview/clients/create
|
||||||
@ -1575,10 +1586,23 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
|
|||||||
clientDir = os.path.join(userDir, "clients", clientId)
|
clientDir = os.path.join(userDir, "clients", clientId)
|
||||||
os.makedirs(clientDir, exist_ok=True)
|
os.makedirs(clientDir, exist_ok=True)
|
||||||
|
|
||||||
profileData = f"# 客户基本档案\n姓名:{name}\n手机号/尾号:{phone}\n建档时间:{time.strftime('%Y-%m-%d')}\n"
|
profileData = f"# 客户基本档案\n姓名:{name}\n手机号/尾号:{phone}\n建档时间:{time.strftime('%Y-%m-%d')}\n\n## 标签\n- 新客\n\n## AI 沉淀洞察\n- 暂无\n"
|
||||||
with open(os.path.join(clientDir, "profile.md"), "w", encoding="utf-8") as f:
|
with open(os.path.join(clientDir, "profile.md"), "w", encoding="utf-8") as f:
|
||||||
f.write(profileData)
|
f.write(profileData)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from hermes_state import SessionDB
|
||||||
|
db = SessionDB()
|
||||||
|
with db._lock:
|
||||||
|
db._conn.execute(
|
||||||
|
"INSERT INTO deepview_clients (client_id, user_id, name, phone) VALUES (?, ?, ?, ?)",
|
||||||
|
(clientId, user["userId"], name, phone)
|
||||||
|
)
|
||||||
|
db._conn.commit()
|
||||||
|
logger.info(f"[DeepviewSSE] Created client {clientId} in DB")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[DeepviewSSE] Failed to create client {clientId} in DB: {e}")
|
||||||
|
|
||||||
return web.json_response({
|
return web.json_response({
|
||||||
"id": clientId,
|
"id": clientId,
|
||||||
"name": name,
|
"name": name,
|
||||||
@ -1654,10 +1678,33 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
|
|||||||
from hermes_state import SessionDB
|
from hermes_state import SessionDB
|
||||||
db = SessionDB()
|
db = SessionDB()
|
||||||
with db._lock:
|
with db._lock:
|
||||||
|
# Update report records to associate with client
|
||||||
|
cursor = db._conn.execute("SELECT report_json FROM deepview_reports_v2 WHERE report_id=? AND user_id=?", (reportId, userId))
|
||||||
|
row = cursor.fetchone()
|
||||||
|
|
||||||
|
# Update clientName directly into the json to avoid joins in reports/list and report/get
|
||||||
|
if row:
|
||||||
|
try:
|
||||||
|
rData = json.loads(row[0])
|
||||||
|
|
||||||
|
# Find client name
|
||||||
|
c_cursor = db._conn.execute("SELECT name FROM deepview_clients WHERE client_id=?", (clientId,))
|
||||||
|
c_row = c_cursor.fetchone()
|
||||||
|
c_name = c_row[0] if c_row else "未知访客"
|
||||||
|
|
||||||
|
rData["clientName"] = c_name
|
||||||
|
db._conn.execute("UPDATE deepview_reports_v2 SET report_json=? WHERE report_id=?", (json.dumps(rData, ensure_ascii=False), reportId))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
db._conn.execute(
|
db._conn.execute(
|
||||||
"UPDATE deepview_reports_v2 SET client_id=?, context_id=? WHERE report_id=? AND user_id=?",
|
"UPDATE deepview_reports_v2 SET client_id=?, context_id=? WHERE report_id=? AND user_id=?",
|
||||||
(clientId, f"recording:{clientId}/{reportId}", reportId, userId)
|
(clientId, f"recording:{clientId}/{reportId}", reportId, userId)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Update client's updated_at timestamp to float to top in the list
|
||||||
|
db._conn.execute("UPDATE deepview_clients SET updated_at=strftime('%s','now') WHERE client_id=?", (clientId,))
|
||||||
|
|
||||||
db._conn.commit()
|
db._conn.commit()
|
||||||
logger.info(f"[DeepviewSSE] DB archived: {reportId} → client {clientId}")
|
logger.info(f"[DeepviewSSE] DB archived: {reportId} → client {clientId}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -1677,6 +1724,27 @@ xray.module5: track1(数组,每项含node/action/strategy/purpose), track2(数
|
|||||||
|
|
||||||
self._app = web.Application()
|
self._app = web.Application()
|
||||||
|
|
||||||
|
# 初始化数据库 schema
|
||||||
|
try:
|
||||||
|
from hermes_state import SessionDB
|
||||||
|
db = SessionDB()
|
||||||
|
with db._lock:
|
||||||
|
db._conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS deepview_clients (
|
||||||
|
client_id TEXT PRIMARY KEY,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
phone TEXT DEFAULT '',
|
||||||
|
created_at REAL DEFAULT (strftime('%s','now')),
|
||||||
|
updated_at REAL DEFAULT (strftime('%s','now'))
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
db._conn.execute("CREATE INDEX IF NOT EXISTS idx_clients_user ON deepview_clients(user_id)")
|
||||||
|
db._conn.commit()
|
||||||
|
logger.info("[DeepviewSSE] DB schema for clients initialized")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[DeepviewSSE] DB schema init failed: {e}")
|
||||||
|
|
||||||
# 注册路由
|
# 注册路由
|
||||||
self._app.router.add_get("/deepview/health", self._handleHealth)
|
self._app.router.add_get("/deepview/health", self._handleHealth)
|
||||||
self._app.router.add_get("/deepview/chat/history", self._handleHistory)
|
self._app.router.add_get("/deepview/chat/history", self._handleHistory)
|
||||||
|
|||||||
@ -62,6 +62,12 @@ export class ApiService {
|
|||||||
}, { headers: this.getHeaders() }).toPromise();
|
}, { headers: this.getHeaders() }).toPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getClients(): Promise<any> {
|
||||||
|
return this.http.get(`${this.apiUrl}/clients/list`, {
|
||||||
|
headers: this.getHeaders()
|
||||||
|
}).toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
async createClient(name: string, phone: string = ''): Promise<any> {
|
async createClient(name: string, phone: string = ''): Promise<any> {
|
||||||
return this.http.post(`${this.apiUrl}/clients/create`, {
|
return this.http.post(`${this.apiUrl}/clients/create`, {
|
||||||
name, phone
|
name, phone
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
export const MOCK_CLIENTS = [
|
|
||||||
{ id: 'p_li001', name: '李梅梅', phone: '8812', age: 38, lastVisit: '2026-03-20', tags: ['乔雅登', '极度怕疼'] },
|
|
||||||
{ id: 'p_wang002', name: '王晓菲', phone: '1024', age: 29, lastVisit: '2026-04-10', tags: ['光子嫩肤'] },
|
|
||||||
{ id: 'p_zhao003', name: '赵佳佳', phone: '5566', age: 45, lastVisit: '2025-12-05', tags: ['超声炮'] },
|
|
||||||
{ id: 'p_unknown', name: '散客/未建档', phone: '', age: 0, lastVisit: '首次面诊', tags: [] }
|
|
||||||
];
|
|
||||||
@ -20,27 +20,27 @@
|
|||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="name-row">
|
<div class="name-row">
|
||||||
<span class="name">{{ client.name }}</span>
|
<span class="name">{{ client.name }}</span>
|
||||||
<span class="tag" [ngClass]="getStageBadgeClass(client.ltcStatus.stage)">
|
<span class="tag" *ngIf="client.ltcStatus?.stage" [ngClass]="getStageBadgeClass(client.ltcStatus?.stage)">
|
||||||
{{ getStageLabel(client.ltcStatus.stage) }}
|
{{ getStageLabel(client.ltcStatus?.stage) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">{{ client.gender }} · {{ client.age }}岁 · {{ client.lastVisit }} 入库</div>
|
<div class="meta">{{ client.gender || '女' }} · {{ client.age || '-' }}岁 · 入库时间: {{ client.createdAt ? (client.createdAt * 1000 | date:'MM-dd HH:mm') : client.lastVisit }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="client-body">
|
<div class="client-body">
|
||||||
<div class="demand-row">
|
<div class="demand-row">
|
||||||
<strong>核心诉求:</strong>{{ client.project }}
|
<strong>核心诉求:</strong>{{ client.project || '待AI挖掘/人工补全' }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="barrier-box" *ngIf="client.ltcStatus.stage !== 'won'">
|
<div class="barrier-box" *ngIf="client.ltcStatus && client.ltcStatus?.stage !== 'won'">
|
||||||
<div class="barrier-title">⚠️ AI 诊断阻力</div>
|
<div class="barrier-title">⚠️ AI 诊断阻力</div>
|
||||||
<div class="barrier-text">{{ client.ltcStatus.coreBarrier }}</div>
|
<div class="barrier-text">{{ client.ltcStatus?.coreBarrier }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="action-box">
|
<div class="action-box" *ngIf="client.ltcStatus">
|
||||||
<div class="action-title">💡 下一步建议</div>
|
<div class="action-title">💡 下一步建议</div>
|
||||||
<div class="action-text">{{ client.ltcStatus.nextAction }}</div>
|
<div class="action-text">{{ client.ltcStatus?.nextAction }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { MOCK_PATIENTS, Patient } from '../../data/mock-data';
|
import { ApiService } from '../../core/api.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-client-ltc-dashboard',
|
selector: 'app-client-ltc-dashboard',
|
||||||
@ -11,7 +11,22 @@ import { MOCK_PATIENTS, Patient } from '../../data/mock-data';
|
|||||||
styleUrl: './client-ltc-dashboard.css',
|
styleUrl: './client-ltc-dashboard.css',
|
||||||
})
|
})
|
||||||
export class ClientLtcDashboard {
|
export class ClientLtcDashboard {
|
||||||
clients = MOCK_PATIENTS;
|
clients: any[] = [];
|
||||||
|
|
||||||
|
constructor(private api: ApiService) {}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.loadClients();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadClients() {
|
||||||
|
try {
|
||||||
|
const res = await this.api.getClients();
|
||||||
|
this.clients = res.clients || [];
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load clients in LTC dashboard', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getStageBadgeClass(stage: string): string {
|
getStageBadgeClass(stage: string): string {
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { RouterLink, ActivatedRoute } from '@angular/router';
|
import { RouterLink, ActivatedRoute } from '@angular/router';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { MOCK_REPORTS, Report } from '../../data/mock-data';
|
import { MOCK_REPORTS, Report } from '../../data/mock-data';
|
||||||
import { MOCK_CLIENTS } from '../../core/client-mock';
|
|
||||||
import { ApiService } from '../../core/api.service';
|
import { ApiService } from '../../core/api.service';
|
||||||
import { AuthService } from '../../core/auth.service';
|
import { AuthService } from '../../core/auth.service';
|
||||||
import { ChatContextService } from '../../core/chat-context.service';
|
import { ChatContextService } from '../../core/chat-context.service';
|
||||||
@ -25,8 +24,8 @@ export class ReportDetail implements OnInit {
|
|||||||
archiveStatus: 'unarchived' | 'archived' = 'unarchived';
|
archiveStatus: 'unarchived' | 'archived' = 'unarchived';
|
||||||
showArchivePanel = false;
|
showArchivePanel = false;
|
||||||
searchQuery = '';
|
searchQuery = '';
|
||||||
clients = MOCK_CLIENTS;
|
clients: any[] = [];
|
||||||
filteredClients = MOCK_CLIENTS;
|
filteredClients: any[] = [];
|
||||||
isArchiving = false;
|
isArchiving = false;
|
||||||
isCreatingClient = false;
|
isCreatingClient = false;
|
||||||
|
|
||||||
@ -40,6 +39,17 @@ export class ReportDetail implements OnInit {
|
|||||||
const realId = decodeURIComponent(id);
|
const realId = decodeURIComponent(id);
|
||||||
this.loadReportFromApi(realId);
|
this.loadReportFromApi(realId);
|
||||||
});
|
});
|
||||||
|
this.loadClients();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadClients() {
|
||||||
|
try {
|
||||||
|
const res = await this.api.getClients();
|
||||||
|
this.clients = res.clients || [];
|
||||||
|
this.filteredClients = [...this.clients];
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to load clients', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 从后端 DB 加载静态报告,失败时降级到 localStorage 缓存 */
|
/** 从后端 DB 加载静态报告,失败时降级到 localStorage 缓存 */
|
||||||
@ -86,7 +96,7 @@ export class ReportDetail implements OnInit {
|
|||||||
filterClients() {
|
filterClients() {
|
||||||
const q = this.searchQuery.toLowerCase();
|
const q = this.searchQuery.toLowerCase();
|
||||||
this.filteredClients = this.clients.filter(c =>
|
this.filteredClients = this.clients.filter(c =>
|
||||||
c.name.includes(q) || c.phone.includes(q)
|
c.name?.toLowerCase().includes(q) || c.phone?.includes(q)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,17 +126,10 @@ export class ReportDetail implements OnInit {
|
|||||||
const name = this.searchQuery.replace(/\d+/g, '').replace('尾号', '').replace(/\(\)/g, '').trim();
|
const name = this.searchQuery.replace(/\d+/g, '').replace('尾号', '').replace(/\(\)/g, '').trim();
|
||||||
|
|
||||||
const res = await this.api.createClient(name, phone);
|
const res = await this.api.createClient(name, phone);
|
||||||
const newClient = {
|
|
||||||
id: res.id,
|
|
||||||
name: res.name,
|
|
||||||
phone: res.phone,
|
|
||||||
age: 30,
|
|
||||||
lastVisit: '首次建档',
|
|
||||||
tags: ['新客']
|
|
||||||
};
|
|
||||||
|
|
||||||
this.clients.push(newClient as any);
|
this.clients.unshift(res); // Add newly created to the top
|
||||||
await this.archiveToClient(newClient);
|
this.filteredClients = [...this.clients];
|
||||||
|
await this.archiveToClient(res);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
alert("建档失败:" + e.message);
|
alert("建档失败:" + e.message);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user