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"
|
|||
|
|
```
|