Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cli-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -26362,8 +26362,15 @@
"school",
"content",
"likes",
"likes_status",
"collects",
"collects_status",
"comments",
"comments_status",
"shares",
"shares_status",
"views",
"views_status",
"time",
"location"
],
Expand Down
18 changes: 13 additions & 5 deletions clis/nowcoder/detail.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { cli } from '@jackwener/opencli/registry';
import { projectNowcoderMetrics } from './metrics.js';

cli({
site: 'nowcoder',
Expand All @@ -9,13 +10,22 @@ cli({
args: [
{ name: 'id', positional: true, required: true, help: 'Post ID, UUID, or URL' },
],
columns: ['title', 'author', 'school', 'content', 'likes', 'comments', 'views', 'time', 'location'],
columns: [
'title', 'author', 'school', 'content',
'likes', 'likes_status',
'collects', 'collects_status',
'comments', 'comments_status',
'shares', 'shares_status',
'views', 'views_status',
'time', 'location',
],
pipeline: [
{ navigate: 'https://www.nowcoder.com' },
{ evaluate: `(async () => {
const raw = \${{ args.id | json }};
const base = 'https://gw-c.nowcoder.com';
const strip = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&').trim();
const projectMetrics = ${projectNowcoderMetrics.toString()};

let id = raw;
const urlMatch = raw.match(/discuss\\/(\\d+)/);
Expand Down Expand Up @@ -44,15 +54,13 @@ cli({
if (!data) throw new Error('Post not found: ' + id);

const user = data.userBrief || {};
const freq = data.frequencyData || {};
const metrics = projectMetrics(data.frequencyData);
return [{
title: data.title || '(untitled)',
author: user.nickname || '',
school: user.educationInfo || '',
content: strip(data.content || '').substring(0, 500),
likes: freq.likeCnt || 0,
comments: freq.commentCnt || freq.totalCommentCnt || 0,
views: freq.viewCnt || 0,
...metrics,
time: data.createdAt ? new Date(data.createdAt).toISOString().slice(0, 19) : '',
location: data.ip4Location || '',
}];
Expand Down
45 changes: 45 additions & 0 deletions clis/nowcoder/metrics.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/**
* Project Nowcoder frequencyData without conflating a real zero with a
* missing or malformed field. This function is serialized into the browser
* evaluation context, so it must remain self-contained.
*/
export function projectNowcoderMetrics(frequencyData) {
const source = frequencyData && typeof frequencyData === 'object'
? frequencyData
: {};
const readMetric = (...keys) => {
for (const key of keys) {
if (!Object.prototype.hasOwnProperty.call(source, key))
continue;
const raw = source[key];
if (raw == null || raw === '')
continue;
const value = typeof raw === 'number' ? raw : Number(String(raw).trim());
if (Number.isInteger(value) && value >= 0) {
return { value, status: 'available' };
}
}
return { value: null, status: 'unavailable' };
};

const likes = readMetric('likeCnt');
// Nowcoder calls this followCnt in the API, while the detail UI labels the
// same interaction 收藏 (collect/save).
const collects = readMetric('followCnt');
const comments = readMetric('commentCnt', 'totalCommentCnt');
const shares = readMetric('shareCnt');
const views = readMetric('viewCnt');

return {
likes: likes.value,
likes_status: likes.status,
collects: collects.value,
collects_status: collects.status,
comments: comments.value,
comments_status: comments.status,
shares: shares.value,
shares_status: shares.status,
views: views.value,
views_status: views.status,
};
}
87 changes: 87 additions & 0 deletions clis/nowcoder/metrics.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { describe, expect, it } from 'vitest';
import { getRegistry } from '@jackwener/opencli/registry';
import { projectNowcoderMetrics } from './metrics.js';
import './detail.js';

describe('Nowcoder interaction metrics', () => {
it('preserves real zero counts as available numbers', () => {
expect(projectNowcoderMetrics({
likeCnt: 0,
followCnt: 0,
commentCnt: 0,
shareCnt: 0,
viewCnt: 0,
})).toEqual({
likes: 0,
likes_status: 'available',
collects: 0,
collects_status: 'available',
comments: 0,
comments_status: 'available',
shares: 0,
shares_status: 'available',
views: 0,
views_status: 'available',
});
});

it('projects the public collect and share counts exposed by Nowcoder', () => {
expect(projectNowcoderMetrics({
likeCnt: 49,
followCnt: 18,
commentCnt: 83,
totalCommentCnt: 105,
shareCnt: 2,
viewCnt: 14369,
})).toMatchObject({
likes: 49,
collects: 18,
comments: 83,
shares: 2,
views: 14369,
});
});

it('uses null plus unavailable instead of inventing zero for absent metrics', () => {
expect(projectNowcoderMetrics({
likeCnt: -1,
commentCnt: null,
totalCommentCnt: '7',
viewCnt: 'not-a-number',
})).toEqual({
likes: null,
likes_status: 'unavailable',
collects: null,
collects_status: 'unavailable',
comments: 7,
comments_status: 'available',
shares: null,
shares_status: 'unavailable',
views: null,
views_status: 'unavailable',
});
});

it('remains self-contained when serialized for browser evaluation', () => {
const serialized = Function(`return (${projectNowcoderMetrics.toString()})`)();
expect(serialized({ likeCnt: 0, followCnt: 3, shareCnt: 1 })).toMatchObject({
likes: 0,
likes_status: 'available',
collects: 3,
collects_status: 'available',
shares: 1,
shares_status: 'available',
});
});

it('declares every metric value and status in detail output', () => {
const columns = getRegistry().get('nowcoder/detail')?.columns || [];
expect(columns).toEqual(expect.arrayContaining([
'likes', 'likes_status',
'collects', 'collects_status',
'comments', 'comments_status',
'shares', 'shares_status',
'views', 'views_status',
]));
});
});
12 changes: 12 additions & 0 deletions docs/adapters/browser/nowcoder.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,15 @@ opencli nowcoder hot -v

- **Public commands** (hot, trending, topics, recommend, creators, companies, jobs): No login required
- **Cookie commands** (all others): Chrome running and **logged into** nowcoder.com, [Browser Bridge extension](/guide/browser-bridge) installed

## Detail interaction metrics

`nowcoder detail` exposes the five public interaction counts returned in Nowcoder's `frequencyData`:

- `likes` comes from `likeCnt`.
- `collects` comes from `followCnt`, which the detail UI labels as 收藏.
- `comments` comes from `commentCnt`, falling back to `totalCommentCnt` when needed.
- `shares` comes from `shareCnt`.
- `views` comes from `viewCnt`.

Each count has a matching `<metric>_status`. A present non-negative integer, including a real `0`, is returned with `available`. A missing or malformed field is returned as `null` with `unavailable`; OpenCLI does not replace unavailable data with a fabricated zero.