f6e553dcc3
- desktop: new DeepSeek page adapter and monitor registry - desktop: extract generic AI auth helpers into shared module and skip binding when challenge signals are present - admin-web: render DeepSeek HTML answers with inline citation anchors and surface reference-number badges on source cards - server: fall back to inline_links/source_panel_links and read text/siteName fields when extracting DeepSeek citations Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
275 lines
8.4 KiB
Markdown
275 lines
8.4 KiB
Markdown
# DeepSeek Monitoring Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Add production-usable `deepseek` desktop monitoring support with answer capture, citation extraction, runtime routing, and regression coverage.
|
||
|
||
**Architecture:** Reuse the existing generic AI auth flow, add a new Playwright-based desktop monitor adapter for DeepSeek, register it in the desktop runtime, and keep server-side result ingestion unchanged. The adapter should favor structured page observation plus network-response capture so answer capture and citation extraction can degrade independently.
|
||
|
||
**Tech Stack:** TypeScript + Vitest in `apps/desktop-client`, Go + `testing` in `server/internal/tenant/app`, Playwright hidden-page execution, pnpm workspace, existing shared monitoring payload contracts.
|
||
|
||
---
|
||
|
||
### Task 1: Lock In Failing Tests First
|
||
|
||
**Files:**
|
||
- Create: `apps/desktop-client/src/main/adapters/deepseek.test.ts`
|
||
- Modify: `server/internal/tenant/app/monitoring_callback_service_test.go`
|
||
|
||
- [ ] **Step 1: Write the failing desktop adapter helper tests**
|
||
|
||
```ts
|
||
import { describe, expect, it } from "vitest";
|
||
|
||
import { __deepseekTestUtils } from "./deepseek";
|
||
|
||
const {
|
||
extractQuestionText,
|
||
dedupeSourceItems,
|
||
classifyDeepseekSources,
|
||
isObservationComplete,
|
||
} = __deepseekTestUtils;
|
||
|
||
describe("deepseek adapter helpers", () => {
|
||
it("extracts question text from monitoring payload", () => {
|
||
expect(extractQuestionText({ question_text: "DeepSeek 怎么看 GEO?" })).toBe("DeepSeek 怎么看 GEO?");
|
||
});
|
||
|
||
it("dedupes sources by normalized url", () => {
|
||
const items = dedupeSourceItems([
|
||
{ url: "https://example.com/a#top", normalized_url: "https://example.com/a#top" },
|
||
{ url: "https://example.com/a#bottom", normalized_url: "https://example.com/a#bottom" },
|
||
]);
|
||
|
||
expect(items).toHaveLength(1);
|
||
expect(items[0]?.normalized_url).toBe("https://example.com/a");
|
||
});
|
||
|
||
it("separates citations from search-style sources", () => {
|
||
const classified = classifyDeepseekSources({
|
||
inlineLinks: [{ url: "https://example.com/citation", normalized_url: "https://example.com/citation" }],
|
||
sourcePanelLinks: [],
|
||
searchPanelLinks: [{ url: "https://example.com/search", normalized_url: "https://example.com/search" }],
|
||
});
|
||
|
||
expect(classified.citations).toHaveLength(1);
|
||
expect(classified.searchResults).toHaveLength(1);
|
||
});
|
||
|
||
it("treats a stable answer as complete even without citations", () => {
|
||
expect(isObservationComplete({
|
||
answer: "这是最终答案",
|
||
busy: false,
|
||
loginRequired: false,
|
||
challengeRequired: false,
|
||
signature: "sig-1",
|
||
})).toBe(true);
|
||
});
|
||
});
|
||
```
|
||
|
||
- [ ] **Step 2: Run the new desktop test to verify it fails**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
pnpm --filter @geo/desktop-client test apps/desktop-client/src/main/adapters/deepseek.test.ts
|
||
```
|
||
|
||
Expected:
|
||
|
||
- FAIL because `./deepseek` does not exist yet
|
||
|
||
- [ ] **Step 3: Write the failing server regression test**
|
||
|
||
```go
|
||
func TestBuildMonitoringRawPayloadDeepSeekIncludesSearchResultsInCitationInputs(t *testing.T) {
|
||
searchTitle := "搜索网页结果"
|
||
|
||
req := MonitoringTaskResultRequest{
|
||
Status: "succeeded",
|
||
SearchResults: []MonitoringSourceItem{
|
||
{
|
||
URL: "https://example.com/search",
|
||
Title: &searchTitle,
|
||
},
|
||
},
|
||
}
|
||
|
||
_, sourceInputs := buildMonitoringRawPayload("deepseek", req)
|
||
if len(sourceInputs) != 1 {
|
||
t.Fatalf("buildMonitoringRawPayload() sourceInputs len = %d, want 1", len(sourceInputs))
|
||
}
|
||
if sourceInputs[0].URL != "https://example.com/search" {
|
||
t.Fatalf("buildMonitoringRawPayload() source url = %q, want search url", sourceInputs[0].URL)
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 4: Run the targeted Go test to verify the new assertion state**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
cd server && go test ./internal/tenant/app -run TestBuildMonitoringRawPayloadDeepSeekIncludesSearchResultsInCitationInputs -count=1
|
||
```
|
||
|
||
Expected:
|
||
|
||
- PASS or FAIL is acceptable here, but the test must compile and pin the intended DeepSeek ingestion behavior before adapter code lands
|
||
|
||
### Task 2: Implement the DeepSeek Desktop Adapter
|
||
|
||
**Files:**
|
||
- Create: `apps/desktop-client/src/main/adapters/deepseek.ts`
|
||
- Modify: `apps/desktop-client/src/main/adapters/index.ts`
|
||
- Modify: `apps/desktop-client/src/main/runtime-controller.ts`
|
||
|
||
- [ ] **Step 1: Add the adapter skeleton and helper exports**
|
||
|
||
```ts
|
||
export const deepseekAdapter: MonitorAdapter = {
|
||
provider: "deepseek",
|
||
executionMode: "playwright",
|
||
async query(context, payload) {
|
||
// implement after tests are in place
|
||
},
|
||
};
|
||
|
||
export const __deepseekTestUtils = {
|
||
extractQuestionText,
|
||
dedupeSourceItems,
|
||
classifyDeepseekSources,
|
||
isObservationComplete,
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 2: Implement page bootstrap and observation helpers**
|
||
|
||
```ts
|
||
async function ensurePageOnDeepSeekChat(page: PlaywrightPage, signal: AbortSignal): Promise<void> {
|
||
if (signal.aborted) {
|
||
throw new Error("adapter_aborted");
|
||
}
|
||
|
||
if (!page.url() || !page.url().startsWith("https://chat.deepseek.com/")) {
|
||
await page.goto("https://chat.deepseek.com/", {
|
||
waitUntil: "domcontentloaded",
|
||
timeout: 20_000,
|
||
});
|
||
}
|
||
|
||
await page.waitForLoadState("domcontentloaded", { timeout: 20_000 }).catch(() => undefined);
|
||
await page.waitForLoadState("networkidle", { timeout: 3_000 }).catch(() => undefined);
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 3: Implement answer submission, wait loop, and citation extraction**
|
||
|
||
```ts
|
||
const result = await waitForDeepSeekAnswer(page, questionText, context.signal);
|
||
const classified = classifyDeepseekSources({
|
||
inlineLinks: result.inlineLinks,
|
||
sourcePanelLinks: result.sourcePanelLinks,
|
||
searchPanelLinks: result.searchPanelLinks,
|
||
});
|
||
|
||
return {
|
||
status: "succeeded",
|
||
summary: "DeepSeek 监控任务执行成功。",
|
||
payload: {
|
||
platform: "deepseek",
|
||
provider_model: result.providerModel ?? "deepseek-chat",
|
||
provider_request_id: result.providerRequestID,
|
||
request_id: result.requestID,
|
||
answer: result.answer,
|
||
citations: toJsonSources(classified.citations),
|
||
search_results: toJsonSources(classified.searchResults),
|
||
raw_response_json: {
|
||
platform: "deepseek",
|
||
page_url: result.url,
|
||
answer: result.answer,
|
||
citations: toJsonSources(classified.citations),
|
||
search_results: toJsonSources(classified.searchResults),
|
||
},
|
||
},
|
||
};
|
||
```
|
||
|
||
- [ ] **Step 4: Register the adapter in exports and runtime routing**
|
||
|
||
```ts
|
||
export * from "./deepseek";
|
||
```
|
||
|
||
```ts
|
||
import { deepseekAdapter } from "./adapters";
|
||
|
||
if (platform === deepseekAdapter.provider) {
|
||
return deepseekAdapter;
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 5: Run the desktop DeepSeek tests to verify they now pass**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
pnpm --filter @geo/desktop-client test apps/desktop-client/src/main/adapters/deepseek.test.ts
|
||
```
|
||
|
||
Expected:
|
||
|
||
- PASS
|
||
|
||
### Task 3: Verify Integration Boundaries
|
||
|
||
**Files:**
|
||
- Modify: `apps/desktop-client/src/main/platform-auth-adapters.ts` only if DeepSeek-specific auth/risk strings are discovered during implementation
|
||
- Modify: `server/internal/tenant/app/monitoring_callback_service_test.go`
|
||
|
||
- [ ] **Step 1: Keep server ingestion behavior explicit for DeepSeek**
|
||
|
||
```go
|
||
func TestBuildMonitoringRawPayloadDeepSeekIncludesSearchResultsInCitationInputs(t *testing.T) {
|
||
// keep DeepSeek aligned with the default non-Kimi ingestion path
|
||
}
|
||
```
|
||
|
||
- [ ] **Step 2: Run the combined targeted verification suite**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
pnpm --filter @geo/desktop-client test apps/desktop-client/src/main/adapters/deepseek.test.ts apps/desktop-client/src/main/platform-auth-adapters.test.ts
|
||
cd server && go test ./internal/tenant/app -run 'TestBuildMonitoringRawPayload(DeepSeek|Qwen|Kimi)|TestExtractMonitoringInlineCitationsFromRawResponseKimi' -count=1
|
||
```
|
||
|
||
Expected:
|
||
|
||
- all targeted tests pass
|
||
|
||
- [ ] **Step 3: Run desktop typecheck**
|
||
|
||
Run:
|
||
|
||
```bash
|
||
pnpm typecheck:desktop
|
||
```
|
||
|
||
Expected:
|
||
|
||
- PASS with no new type errors from the DeepSeek adapter wiring
|
||
|
||
- [ ] **Step 4: Commit the implementation**
|
||
|
||
```bash
|
||
git add apps/desktop-client/src/main/adapters/deepseek.ts \
|
||
apps/desktop-client/src/main/adapters/deepseek.test.ts \
|
||
apps/desktop-client/src/main/adapters/index.ts \
|
||
apps/desktop-client/src/main/runtime-controller.ts \
|
||
server/internal/tenant/app/monitoring_callback_service_test.go \
|
||
docs/superpowers/plans/2026-04-23-deepseek-monitoring.md
|
||
git commit -m "feat(desktop): add DeepSeek monitoring adapter"
|
||
```
|